├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .stylelintrc.js ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── config.dev.ts ├── config.ts ├── constant.ts ├── defaultSettings.ts ├── permission.ts ├── proxy.ts ├── routes.ts └── theme.ts ├── docker ├── docker-compose.dev.yml ├── docker-compose.yml └── nginx.conf ├── jest.config.js ├── jsconfig.json ├── mock ├── listTableList.ts ├── notices.ts ├── route.ts └── user.ts ├── package.json ├── public ├── favicon.ico ├── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png ├── images │ ├── addCus_step1.jpg │ ├── addCus_step10.jpg │ ├── addCus_step11.jpg │ ├── addCus_step12.jpg │ ├── addCus_step2.jpg │ ├── addCus_step3.jpg │ ├── addCus_step4.jpg │ ├── addCus_step5.jpg │ ├── addCus_step6.jpg │ ├── addCus_step7.jpg │ ├── addCus_step8.jpg │ └── addCus_step9.jpg ├── logo.png └── openscrm_icon.svg ├── src ├── assets │ ├── avatar-default.svg │ ├── chat.svg │ ├── damaged-image.png │ ├── damaged-image.svg │ ├── default-image.png │ ├── default-image.svg │ ├── empty.svg │ ├── excel-png.png │ ├── external.svg │ ├── file-icon-excel.svg │ ├── file-icon-image.svg │ ├── file-icon-link.svg │ ├── file-icon-pdf.svg │ ├── file-icon-ppt.svg │ ├── file-icon-video.svg │ ├── file-icon-word.svg │ ├── group-chat-icon.svg │ ├── group-chat.svg │ ├── logo.png │ ├── logo.svg │ ├── logo_black.svg │ ├── logo_en_h.svg │ ├── logo_horizontal.svg │ ├── logo_white.svg │ ├── pdf-png.png │ ├── phone.png │ ├── ppt-png.png │ ├── qrcode.png │ ├── tag.svg │ ├── uploadimage.svg │ └── word-png.png ├── components │ ├── Authorized │ │ ├── Authorized.tsx │ │ ├── AuthorizedRoute.tsx │ │ ├── CheckPermissions.tsx │ │ ├── PromiseRender.tsx │ │ ├── Secured.tsx │ │ ├── index.tsx │ │ └── renderAuthorize.ts │ ├── GlobalHeader │ │ ├── RightContent.tsx │ │ ├── StaffAdminAvatarDropdown.tsx │ │ └── index.less │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ └── PageLoading │ │ └── index.tsx ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ └── baseLayout.e2e.js ├── global.less ├── global.tsx ├── layouts │ ├── BasicLayout.tsx │ ├── BlankLayout.tsx │ ├── LoginLayout.less │ ├── LoginLayout.tsx │ └── StaffAdminSecurityLayout.tsx ├── locales │ ├── en-US.ts │ ├── en-US │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-CN.ts │ └── zh-CN │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts ├── manifest.json ├── models │ ├── connect.d.ts │ ├── global.ts │ ├── setting.ts │ └── staffAdmin.ts ├── pages │ ├── 404.tsx │ ├── StaffAdmin │ │ ├── ChatSession │ │ │ ├── components │ │ │ │ ├── MessageView.tsx │ │ │ │ ├── SearchMsgView.tsx │ │ │ │ └── index.less │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Components │ │ │ ├── Columns │ │ │ │ ├── CollapsedStaffs │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── CollapsedTags │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ ├── Fields │ │ │ │ ├── AutoReply │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomerTagSelect │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── DepartmentTreeSelect │ │ │ │ │ └── index.tsx │ │ │ │ ├── EditableTag │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── GroupChatSelect │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── GroupChatTagSelect │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── ImageUploader │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── NumberRangeInput │ │ │ │ │ └── index.tsx │ │ │ │ └── StaffTreeSelect │ │ │ │ │ └── index.tsx │ │ │ ├── Modals │ │ │ │ ├── AutoReplyPreviewModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomerTagSelectionModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── DepartmentSelectionModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── GroupChatSelectionModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── GroupChatTagSelectionModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── StaffTreeSelectionModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ └── Sections │ │ │ │ └── AutoReplyPreview │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── ContactWay │ │ │ ├── Components │ │ │ │ ├── form.less │ │ │ │ └── form.tsx │ │ │ ├── copy.tsx │ │ │ ├── create.less │ │ │ ├── create.tsx │ │ │ ├── data.d.ts │ │ │ ├── edit.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Customer │ │ │ ├── Components │ │ │ │ ├── Events.tsx │ │ │ │ ├── InternalTagModal.tsx │ │ │ │ ├── TableInput.tsx │ │ │ │ └── index.less │ │ │ ├── customerDetail.less │ │ │ ├── data.d.ts │ │ │ ├── detail.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── CustomerLoss │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── CustomerMassMsg │ │ │ ├── Components │ │ │ │ ├── form.less │ │ │ │ └── form.tsx │ │ │ ├── create.tsx │ │ │ ├── data.d.ts │ │ │ ├── detail.less │ │ │ ├── detail.tsx │ │ │ ├── edit.tsx │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── CustomerTag │ │ │ ├── Components │ │ │ │ └── createModalForm.tsx │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── CustomerWelcomeMsg │ │ │ ├── Components │ │ │ │ ├── form.less │ │ │ │ └── form.tsx │ │ │ ├── create.tsx │ │ │ ├── data.d.ts │ │ │ ├── edit.tsx │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── DeleteCustomerRecord │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── GroupChat │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── GroupChatTag │ │ │ ├── Components │ │ │ │ └── createModalForm.tsx │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Login │ │ │ ├── callback.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── qrcode.css │ │ ├── MaterialLibrary │ │ │ ├── Article │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── Material │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── TagProvider.tsx │ │ │ ├── components │ │ │ │ ├── MaterialCard │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── TagModal │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── Uploader │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Role │ │ │ ├── Components │ │ │ │ ├── form.tsx │ │ │ │ └── index.less │ │ │ ├── create.tsx │ │ │ ├── data.d.ts │ │ │ ├── edit.tsx │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── ScriptLibrary │ │ │ ├── EnterpriseScript.tsx │ │ │ ├── GroupModal.tsx │ │ │ ├── IndividualScript.tsx │ │ │ ├── ScriptModal.tsx │ │ │ ├── TeamScript.tsx │ │ │ ├── components │ │ │ │ ├── ExpandableParagraph.tsx │ │ │ │ ├── ScriptContentPreView.tsx │ │ │ │ ├── Uploader │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── index.less │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Staff │ │ │ ├── StaffDetails │ │ │ │ └── index.tsx │ │ │ ├── components │ │ │ │ └── DepartmentTree.tsx │ │ │ ├── data.d.ts │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ │ ├── Tutorials │ │ │ └── addCustomer.tsx │ │ └── Welcome │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── service.ts │ └── document.ejs ├── service-worker.js ├── services │ ├── common.ts │ ├── customer_tag_group.ts │ ├── department.ts │ ├── group_chat.ts │ ├── staff.ts │ └── staffAdmin.ts ├── styles │ ├── addCustomerTutorial.less │ └── component.less ├── theme.less ├── typings.d.ts └── utils │ ├── Authorized.ts │ ├── authority.ts │ ├── request.ts │ ├── utils.less │ ├── utils.test.ts │ └── utils.ts ├── tests ├── PuppeteerEnvironment.js ├── beforeTest.js ├── getBrowser.js └── run-tests.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | public 6 | dist 7 | .umi 8 | mock -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | 42 | scripts/deploy* 43 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/node:latest-browsers 2 | 3 | WORKDIR /usr/src/app/ 4 | USER root 5 | COPY package.json ./ 6 | RUN yarn 7 | 8 | COPY ./ ./ 9 | 10 | RUN npm run test:all 11 | 12 | RUN npm run fetch:blocks 13 | 14 | CMD ["npm", "run", "build"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 |

6 | 安全,强大,易开发的企业微信SCRM 7 |

8 | 9 | [安装](#如何安装) | 10 | [截图](#项目截图) 11 | 12 | ### 项目简介 13 | 14 | > 此项目为OpenSCRM管理后台前端项目 15 | 16 | ### 如何安装 17 | - node版本要求:node == 16.20.2 18 | - 修改config/proxy.ts,将后端接口地址修改为你的后端服务地址,如:http://127.0.0.1:9000/ 19 | ```shell 20 | dev: { #开发环境 21 | '/api/': { 22 | target: 'http://127.0.0.1:9000/', # 后端接口地址 23 | changeOrigin: true, 24 | pathRewrite: { '^': '' }, 25 | }, 26 | }, 27 | ``` 28 | 29 | - 国内npm源加速(可选) 30 | ```shell 31 | npm config set registry https://registry.npmmirror.com 32 | ``` 33 | 34 | - 安装项目依赖 35 | ```shell 36 | npm install 37 | ``` 38 | 39 | - 修改配置文件(可选) 40 | ```shell 41 | config/config.ts 42 | ``` 43 | 44 | 45 | - 启动开发环境 46 | ```shell 47 | npm run start 48 | ``` 49 | 50 | 此前端项目是基于Antd Pro的,备查文档:https://pro.ant.design/zh-CN/docs/overview 51 | 52 | ### 项目截图 53 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E7%99%BB%E5%BD%95.png) 54 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E5%90%8E%E5%8F%B0%E9%A6%96%E9%A1%B5.png) 55 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E4%BF%AE%E6%94%B9%E6%B8%A0%E9%81%93%E6%B4%BB%E7%A0%81.png) 56 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E4%BC%9A%E8%AF%9D%E5%AD%98%E6%A1%A3.png) 57 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E4%BF%AE%E6%94%B9%E6%B8%A0%E9%81%93%E6%B4%BB%E7%A0%812.png) 58 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E5%AE%A2%E6%88%B7%E6%A0%87%E7%AD%BE%E7%AE%A1%E7%90%86.png) 59 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E4%BF%AE%E6%94%B9%E7%BE%A4%E5%8F%91.png) 60 | ![](https://openscrm.oss-cn-hangzhou.aliyuncs.com/public/screenshots/%E4%BF%AE%E6%94%B9%E6%AC%A2%E8%BF%8E%E8%AF%AD.png) 61 | 62 | ### 技术栈 63 | * [React](https://zh-hans.reactjs.org/) 64 | * [TypeScript](https://www.tslang.cn/docs/handbook/typescript-in-5-minutes.html) 65 | * [Ant Design](https://ant.design/components/overview-cn/) 66 | * [Ant Design Pro](https://pro.ant.design/zh-CN/docs/overview) 67 | * [Pro Components](https://procomponents.ant.design/components) 68 | 69 | ### 联系作者 70 | 71 | 72 | 73 | 扫码可加入交流群 74 | 75 | ### 版权声明 76 | 77 | OpenSCRM遵循Apache2.0协议,可免费商用 78 | -------------------------------------------------------------------------------- /config/config.dev.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from 'umi'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | // https://github.com/zthxxx/react-dev-inspector 7 | 'react-dev-inspector/plugins/umi/react-inspector', 8 | ], 9 | // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props 10 | inspectorConfig: { 11 | exclude: [], 12 | babelPlugins: [], 13 | babelOptions: {}, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import {defineConfig} from 'umi'; 3 | import proxy from './proxy'; 4 | import routes from './routes'; 5 | import theme from './theme'; 6 | 7 | const {REACT_APP_ENV} = process.env; 8 | 9 | export default defineConfig({ 10 | hash: true, 11 | antd: {}, 12 | dva: { 13 | hmr: true, 14 | }, 15 | history: { 16 | type: 'browser', 17 | }, 18 | dynamicImport: { 19 | loading: '@/components/PageLoading/index', 20 | }, 21 | targets: { 22 | ie: 11, 23 | }, 24 | // umi routes: https://umijs.org/docs/routing 25 | routes, 26 | // https://umijs.org/zh-CN/config#theme 27 | theme: theme, 28 | title: false, 29 | ignoreMomentLocale: true, 30 | proxy: proxy[REACT_APP_ENV || 'dev'], 31 | manifest: { 32 | basePath: '/', 33 | }, 34 | // 快速刷新功能 https://umijs.org/config#fastrefresh 35 | fastRefresh: {}, 36 | esbuild: {}, 37 | // webpack5: {}, 38 | request: { 39 | dataField: 'data', 40 | }, 41 | scripts: [ 42 | 'https://unpkg.com/react@17/umd/react.production.min.js', 43 | 'https://unpkg.com/react-dom@17/umd/react-dom.production.min.js', 44 | 'https://unpkg.com/@ant-design/charts@1.0.5/dist/charts.min.js', 45 | //使用 组织架构图、流程图、资金流向图、缩进树图 才需要使用 46 | //'https://unpkg.com/@ant-design/charts@1.0.5/dist/charts_g6.min.js', 47 | ], 48 | externals: { 49 | react: 'React', 50 | 'react-dom': 'ReactDOM', 51 | "@ant-design/charts": "charts" 52 | }, 53 | devServer: { 54 | host: 'localhost', 55 | port: 9000, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /config/constant.ts: -------------------------------------------------------------------------------- 1 | export const StaffAdminApiPrefix = '/api/v1/staff-admin'; 2 | export const CorpAdminApiPrefix = '/api/v1/corp-admin'; 3 | export const CodeOK = 0; 4 | export const Enable = 1; 5 | export const True = 1; 6 | export const Disable = 2; 7 | export const False = 2; 8 | export const StaffAdminAuthority = 'staffAdmin'; 9 | 10 | // 1=已激活,2=已禁用,4=未激活,5=退出企业 11 | export const StaffActive = 1; 12 | 13 | // localStorage存储key定义 14 | export const LSAdminType = 'adminType'; 15 | export const LSAuthority = 'authority'; 16 | export const LSExtStaffAdminID = 'extStaffAdminID'; 17 | export const LSExtCorpID = 'extCorpID'; 18 | 19 | // 角色定义 20 | export const RoleTypeSuperAdmin = 'superAdmin'; 21 | export const RoleTypeAdmin = 'admin'; 22 | export const RoleTypeDepartmentAdmin = 'departmentAdmin'; 23 | export const RoleTypeStaff = 'staff'; 24 | 25 | // 日期格式定义 26 | export const DateTimeLayout = 'YYYY-MM-DD HH:mm'; 27 | export const TimeLayout = 'HH:mm'; 28 | 29 | export const RoleMap = { 30 | [RoleTypeSuperAdmin]: '超级管理员', 31 | [RoleTypeAdmin]: '管理员', 32 | [RoleTypeDepartmentAdmin]: '部门管理员', 33 | [RoleTypeStaff]: '普通员工', 34 | }; 35 | 36 | export const RoleColorMap = { 37 | [RoleTypeSuperAdmin]: '#ff9318', 38 | [RoleTypeAdmin]: '#1890ff', 39 | [RoleTypeDepartmentAdmin]: '#1890ff', 40 | [RoleTypeStaff]: '#8b9fbb', 41 | }; 42 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as ProSettings } from '@ant-design/pro-layout'; 2 | 3 | type DefaultSettings = Partial & { 4 | pwa: boolean; 5 | }; 6 | 7 | const proSettings: DefaultSettings = { 8 | navTheme: 'dark', 9 | // 拂晓蓝 10 | primaryColor: '#0070cc', 11 | layout: 'side', 12 | contentWidth: 'Fluid', 13 | fixedHeader: false, 14 | fixSiderbar: true, 15 | colorWeak: false, 16 | title: 'OpenSCRM', 17 | pwa: false, 18 | iconfontUrl: '//at.alicdn.com/t/font_2664214_7d8mhwhp1uv.js', 19 | }; 20 | 21 | export type { DefaultSettings }; 22 | 23 | export default proSettings; 24 | -------------------------------------------------------------------------------- /config/permission.ts: -------------------------------------------------------------------------------- 1 | export const HiddenRoute = 'HiddenRoute'; 2 | export const BizQuickReply_Read = 'BizQuickReply_Read'; 3 | export const BizQuickReply_Full = 'BizQuickReply_Full'; 4 | export const BizQuickReplyGroup_Read = 'BizQuickReplyGroup_Read'; 5 | export const BizQuickReplyGroup_Full = 'BizQuickReplyGroup_Full'; 6 | export const BizDepartment_Read = 'BizDepartment_Read'; 7 | export const BizDepartment_Full = 'BizDepartment_Full'; 8 | export const BizMassMsg_Read = 'BizMassMsg_Read'; 9 | export const BizMassMsg_Full = 'BizMassMsg_Full'; 10 | export const BizCustomerRemark_Read = 'BizCustomerRemark_Read'; 11 | export const BizCustomerRemark_Full = 'BizCustomerRemark_Full'; 12 | export const BizCustomerTag_Read = 'BizCustomerTag_Read'; 13 | export const BizCustomerTag_Full = 'BizCustomerTag_Full'; 14 | export const BizCustomerInfo_Read = 'BizCustomerInfo_Read'; 15 | export const BizCustomerInfo_Full = 'BizCustomerInfo_Full'; 16 | export const BizStaffInfo_Read = 'BizStaffInfo_Read'; 17 | export const BizStaffInfo_Full = 'BizStaffInfo_Full'; 18 | export const BizContactWay_Read = 'BizContactWay_Read'; 19 | export const BizContactWay_Full = 'BizContactWay_Full'; 20 | export const BizDeleteCustomer_Read = 'BizDeleteCustomer_Read'; 21 | export const BizDeleteCustomer_Full = 'BizDeleteCustomer_Full'; 22 | export const BizWelcomeMsg_Read = 'BizWelcomeMsg_Read'; 23 | export const BizWelcomeMsg_Full = 'BizWelcomeMsg_Full'; 24 | export const BizCustomerGroupChat_Full = 'BizCustomerGroupChat_Full'; 25 | export const BizCustomerGroupChat_Read = 'BizCustomerGroupChat_Read'; 26 | export const BizCustomerLoss_Full = 'BizCustomerLoss_Full'; 27 | export const BizCustomerLoss_Read = 'BizCustomerLoss_Read'; 28 | export const BizRole_Full = 'BizRole_Full'; 29 | export const BizRole_Read = 'BizRole_Read'; 30 | export const BizMsgArch_Full = 'BizMsgArch_Full'; 31 | export const BizMsgArch_Read = 'BizMsgArch_Read'; 32 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * ------------------------------- 4 | * The agent cannot take effect in the production environment 5 | * so there is no configuration of the production environment 6 | * For details, please see 7 | * https://pro.ant.design/docs/deploy 8 | */ 9 | export default { 10 | dev: { 11 | '/api/': { 12 | target: 'http://127.0.0.1:9001/', 13 | changeOrigin: true, 14 | pathRewrite: { '^': '' }, 15 | }, 16 | }, 17 | test: { 18 | '/api/': { 19 | target: '', 20 | changeOrigin: true, 21 | pathRewrite: { '^': '' }, 22 | }, 23 | }, 24 | pre: { 25 | '/api/': { 26 | target: 'your pre url', 27 | changeOrigin: true, 28 | pathRewrite: { '^': '' }, 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /config/theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '@primary-color': '#2d8cf0', 3 | '@success-color': '#19be6b', 4 | '@text-color': '#12141a', 5 | '@font-family': 6 | 'Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,"\\5FAE\\8F6F\\96C5\\9ED1",Arial,sans-serif;', 7 | '@font-size-base': '14px', 8 | '@border-radius-base': '4px', 9 | '@border-radius-sm': '4px', 10 | '@text-color-secondary': '#55585c', 11 | 12 | // Layout 13 | '@layout-body-background': '#f0f2f5', 14 | '@layout-header-background': '#141f33', 15 | '@layout-header-height': '58px', 16 | '@layout-header-padding': '0 50px', 17 | '@layout-header-color': '@text-color', 18 | '@layout-footer-padding': '24px 50px', 19 | '@layout-footer-background': '@layout-body-background', 20 | '@layout-sider-background': '@layout-header-background', 21 | '@layout-trigger-height': '58px', 22 | '@layout-trigger-background': '#002140', 23 | '@layout-trigger-color': '#fff', 24 | '@layout-zero-trigger-width': '36px', 25 | '@layout-zero-trigger-height': '42px', 26 | // Layout light theme 27 | '@layout-sider-background-light': '#fff', 28 | '@layout-trigger-background-light': '#fff', 29 | '@layout-trigger-color-light': '@text-color', 30 | }; 31 | -------------------------------------------------------------------------------- /docker/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | ant-design-pro_dev: 5 | ports: 6 | - 8000:8000 7 | build: 8 | context: ../ 9 | dockerfile: Dockerfile.dev 10 | container_name: 'ant-design-pro_dev' 11 | volumes: 12 | - ../src:/usr/src/app/src 13 | - ../config:/usr/src/app/config 14 | - ../mock:/usr/src/app/mock 15 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | ant-design-pro_build: 5 | build: ../ 6 | container_name: 'ant-design-pro_build' 7 | volumes: 8 | - dist:/usr/src/app/dist 9 | 10 | ant-design-pro_web: 11 | image: nginx 12 | ports: 13 | - 80:80 14 | container_name: 'ant-design-pro_web' 15 | restart: unless-stopped 16 | volumes: 17 | - dist:/usr/share/nginx/html:ro 18 | - ./nginx.conf:/etc/nginx/conf.d/default.conf 19 | 20 | volumes: 21 | dist: 22 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | # gzip config 4 | gzip on; 5 | gzip_min_length 1k; 6 | gzip_comp_level 9; 7 | gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; 8 | gzip_vary on; 9 | gzip_disable "MSIE [1-6]\."; 10 | 11 | root /usr/share/nginx/html; 12 | include /etc/nginx/mime.types; 13 | location / { 14 | try_files $uri $uri/ /index.html; 15 | } 16 | location /api { 17 | proxy_pass https://proapi.azurewebsites.net; 18 | proxy_set_header X-Forwarded-Proto $scheme; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | testEnvironment: './tests/PuppeteerEnvironment', 4 | verbose: false, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mock/route.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '/api/auth_routes': { 3 | '/form/advanced-form': { authority: ['admin', 'user'] }, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/images/addCus_step1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step1.jpg -------------------------------------------------------------------------------- /public/images/addCus_step10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step10.jpg -------------------------------------------------------------------------------- /public/images/addCus_step11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step11.jpg -------------------------------------------------------------------------------- /public/images/addCus_step12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step12.jpg -------------------------------------------------------------------------------- /public/images/addCus_step2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step2.jpg -------------------------------------------------------------------------------- /public/images/addCus_step3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step3.jpg -------------------------------------------------------------------------------- /public/images/addCus_step4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step4.jpg -------------------------------------------------------------------------------- /public/images/addCus_step5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step5.jpg -------------------------------------------------------------------------------- /public/images/addCus_step6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step6.jpg -------------------------------------------------------------------------------- /public/images/addCus_step7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step7.jpg -------------------------------------------------------------------------------- /public/images/addCus_step8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step8.jpg -------------------------------------------------------------------------------- /public/images/addCus_step9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/images/addCus_step9.jpg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/public/logo.png -------------------------------------------------------------------------------- /src/assets/avatar-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/damaged-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/damaged-image.png -------------------------------------------------------------------------------- /src/assets/default-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/default-image.png -------------------------------------------------------------------------------- /src/assets/excel-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/excel-png.png -------------------------------------------------------------------------------- /src/assets/external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/file-icon-excel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/file-icon-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/file-icon-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/file-icon-ppt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/file-icon-video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/file-icon-word.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/group-chat-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo仅图形 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/logo_en_h.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pdf-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/pdf-png.png -------------------------------------------------------------------------------- /src/assets/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/phone.png -------------------------------------------------------------------------------- /src/assets/ppt-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/ppt-png.png -------------------------------------------------------------------------------- /src/assets/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/qrcode.png -------------------------------------------------------------------------------- /src/assets/tag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/word-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/word-png.png -------------------------------------------------------------------------------- /src/components/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | import check from './CheckPermissions'; 4 | import type { IAuthorityType } from './CheckPermissions'; 5 | import type AuthorizedRoute from './AuthorizedRoute'; 6 | import type Secured from './Secured'; 7 | 8 | type AuthorizedProps = { 9 | authority: IAuthorityType; 10 | noMatch?: React.ReactNode; 11 | }; 12 | 13 | type IAuthorizedType = React.FunctionComponent & { 14 | Secured: typeof Secured; 15 | check: typeof check; 16 | AuthorizedRoute: typeof AuthorizedRoute; 17 | }; 18 | 19 | const Authorized: React.FunctionComponent = ({ 20 | children, 21 | authority, 22 | noMatch = ( 23 | 28 | ), 29 | }) => { 30 | const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; 31 | const dom = check(authority, childrenRender, noMatch); 32 | return <>{dom}; 33 | }; 34 | 35 | export default Authorized as IAuthorizedType; 36 | -------------------------------------------------------------------------------- /src/components/Authorized/AuthorizedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | 3 | import React from 'react'; 4 | import Authorized from './Authorized'; 5 | import type { IAuthorityType } from './CheckPermissions'; 6 | 7 | type AuthorizedRouteProps = { 8 | currentAuthority: string; 9 | component: React.ComponentClass; 10 | render: (props: any) => React.ReactNode; 11 | redirectPath: string; 12 | authority: IAuthorityType; 13 | }; 14 | 15 | const AuthorizedRoute: React.SFC = ({ 16 | component: Component, 17 | render, 18 | authority, 19 | redirectPath, 20 | ...rest 21 | }) => ( 22 | } />} 25 | > 26 | (Component ? : render(props))} 29 | /> 30 | 31 | ); 32 | 33 | export default AuthorizedRoute; 34 | -------------------------------------------------------------------------------- /src/components/Authorized/CheckPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; 3 | // eslint-disable-next-line import/no-cycle 4 | import PromiseRender from './PromiseRender'; 5 | 6 | export type IAuthorityType = 7 | | undefined 8 | | string 9 | | string[] 10 | | Promise 11 | | ((currentAuthority: string | string[]) => IAuthorityType); 12 | 13 | /** 14 | * @en-US 15 | * General permission.ts check method 16 | * Common check permissions method 17 | * @param {Permission judgment} authority 18 | * @param {Your permission.ts | Your permission.ts description} currentAuthority 19 | * @param {Passing components} target 20 | * @param {no pass components | no pass components} Exception 21 | * ------------------------------------------------------- 22 | * @zh-CN 23 | * 通用权限检查方法 Common check permissions method 24 | * 25 | * @param { 权限判定 | Permission judgment } authority 26 | * @param { 你的权限 | Your permission.ts description } currentAuthority 27 | * @param { 通过的组件 | Passing components } target 28 | * @param { 未通过的组件 | no pass components } Exception 29 | */ 30 | const checkPermissions = ( 31 | authority: IAuthorityType, 32 | currentAuthority: string | string[], 33 | target: T, 34 | Exception: K, 35 | ): T | K | React.ReactNode => { 36 | // No judgment permission.ts. View all by default 37 | // Retirement authority, return target; 38 | if (!authority) { 39 | return target; 40 | } 41 | // Array processing 42 | if (Array.isArray(authority)) { 43 | if (Array.isArray(currentAuthority)) { 44 | if (currentAuthority.some((item) => authority.includes(item))) { 45 | return target; 46 | } 47 | } else if (authority.includes(currentAuthority)) { 48 | return target; 49 | } 50 | return Exception; 51 | } 52 | // Deal with string 53 | if (typeof authority === 'string') { 54 | if (Array.isArray(currentAuthority)) { 55 | if (currentAuthority.some((item) => authority === item)) { 56 | return target; 57 | } 58 | } else if (authority === currentAuthority) { 59 | return target; 60 | } 61 | return Exception; 62 | } 63 | // Deal with promise 64 | if (authority instanceof Promise) { 65 | return ok={target} error={Exception} promise={authority} />; 66 | } 67 | // Deal with function 68 | if (typeof authority === 'function') { 69 | const bool = authority(currentAuthority); 70 | // The return value after the function is executed is Promise 71 | if (bool instanceof Promise) { 72 | return ok={target} error={Exception} promise={bool} />; 73 | } 74 | if (bool) { 75 | return target; 76 | } 77 | return Exception; 78 | } 79 | throw new Error('unsupported parameters'); 80 | }; 81 | 82 | export { checkPermissions }; 83 | 84 | function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { 85 | return checkPermissions(authority, CURRENT, target, Exception); 86 | } 87 | 88 | export default check; 89 | -------------------------------------------------------------------------------- /src/components/Authorized/PromiseRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; 5 | // eslint-disable-next-line import/no-cycle 6 | 7 | type PromiseRenderProps = { 8 | ok: T; 9 | error: K; 10 | promise: Promise; 11 | }; 12 | 13 | type PromiseRenderState = { 14 | component: React.ComponentClass | React.FunctionComponent; 15 | }; 16 | 17 | export default class PromiseRender extends React.Component< 18 | PromiseRenderProps, 19 | PromiseRenderState 20 | > { 21 | state: PromiseRenderState = { 22 | component: () => null, 23 | }; 24 | 25 | componentDidMount(): void { 26 | this.setRenderComponent(this.props); 27 | } 28 | 29 | shouldComponentUpdate = ( 30 | nextProps: PromiseRenderProps, 31 | nextState: PromiseRenderState, 32 | ): boolean => { 33 | const { component } = this.state; 34 | if (!isEqual(nextProps, this.props)) { 35 | this.setRenderComponent(nextProps); 36 | } 37 | if (nextState.component !== component) return true; 38 | return false; 39 | }; 40 | 41 | // set render Component : ok or error 42 | setRenderComponent(props: PromiseRenderProps): void { 43 | const ok = this.checkIsInstantiation(props.ok); 44 | const error = this.checkIsInstantiation(props.error); 45 | props.promise 46 | .then(() => { 47 | this.setState({ 48 | component: ok, 49 | }); 50 | return true; 51 | }) 52 | .catch(() => { 53 | this.setState({ 54 | component: error, 55 | }); 56 | }); 57 | } 58 | 59 | // Determine whether the incoming component has been instantiated 60 | // AuthorizedRoute is already instantiated 61 | // Authorized render is already instantiated, children is no instantiated 62 | // Secured is not instantiated 63 | checkIsInstantiation = ( 64 | target: React.ReactNode | React.ComponentClass, 65 | ): React.FunctionComponent => { 66 | if (isComponentClass(target)) { 67 | const Target = target as React.ComponentClass; 68 | return (props: any) => ; 69 | } 70 | if (React.isValidElement(target)) { 71 | return (props: any) => React.cloneElement(target, props); 72 | } 73 | return () => target as React.ReactNode & null; 74 | }; 75 | 76 | render() { 77 | const { component: Component } = this.state; 78 | const { ok, error, promise, ...rest } = this.props; 79 | 80 | return Component ? ( 81 | 82 | ) : ( 83 |
92 | 93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Authorized/index.tsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import Secured from './Secured'; 3 | import check from './CheckPermissions'; 4 | import renderAuthorize from './renderAuthorize'; 5 | 6 | Authorized.Secured = Secured; 7 | Authorized.check = check; 8 | 9 | const RenderAuthorize = renderAuthorize(Authorized); 10 | 11 | export default RenderAuthorize; 12 | -------------------------------------------------------------------------------- /src/components/Authorized/renderAuthorize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-mutable-exports */ 3 | let CURRENT: string | string[] = 'NULL'; 4 | 5 | type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); 6 | /** 7 | * Use authority or getAuthority 8 | * 9 | * @param {string|()=>String} currentAuthority 10 | */ 11 | const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( 12 | currentAuthority: CurrentAuthorityType, 13 | ): T => { 14 | if (currentAuthority) { 15 | if (typeof currentAuthority === 'function') { 16 | CURRENT = currentAuthority(); 17 | } 18 | if ( 19 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 20 | Array.isArray(currentAuthority) 21 | ) { 22 | CURRENT = currentAuthority as string[]; 23 | } 24 | } else { 25 | CURRENT = 'NULL'; 26 | } 27 | return Authorized; 28 | }; 29 | 30 | export { CURRENT }; 31 | export default (Authorized: T) => renderAuthorize(Authorized); 32 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.tsx: -------------------------------------------------------------------------------- 1 | import { Tag, Tooltip } from 'antd'; 2 | import type { Settings as ProSettings } from '@ant-design/pro-layout'; 3 | import { QuestionCircleOutlined } from '@ant-design/icons'; 4 | import React from 'react'; 5 | import type { ConnectProps } from 'umi'; 6 | import { connect } from 'umi'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import styles from './index.less'; 9 | import StaffAdminAvatarDropdown from '@/components/GlobalHeader/StaffAdminAvatarDropdown'; 10 | 11 | export type GlobalHeaderRightProps = { 12 | theme?: ProSettings['navTheme'] | 'realDark'; 13 | } & Partial & 14 | Partial; 15 | 16 | const ENVTagColor = { 17 | dev: 'orange', 18 | test: 'green', 19 | pre: '#87d068', 20 | }; 21 | 22 | const GlobalHeaderRight: React.SFC = (props) => { 23 | const { theme, layout } = props; 24 | let className = styles.right; 25 | 26 | if (theme === 'dark' && layout === 'top') { 27 | className = `${styles.right} ${styles.dark}`; 28 | } 29 | 30 | return ( 31 |
32 | 33 | 41 | 42 | 43 | 44 | 45 | {REACT_APP_ENV && ( 46 | 47 | {REACT_APP_ENV} 48 | 49 | )} 50 |
51 | ); 52 | }; 53 | 54 | export default connect(({ settings }: ConnectState) => ({ 55 | theme: settings.navTheme, 56 | layout: settings.layout, 57 | }))(GlobalHeaderRight); 58 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '../../theme.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | display: flex; 16 | float: right; 17 | height: 48px; 18 | margin-left: auto; 19 | overflow: hidden; 20 | .action { 21 | display: flex; 22 | align-items: center; 23 | height: 100%; 24 | padding: 0 12px; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | > span { 28 | vertical-align: middle; 29 | } 30 | &:hover { 31 | background: @pro-header-hover-bg; 32 | } 33 | &:global(.opened) { 34 | background: @pro-header-hover-bg; 35 | } 36 | } 37 | .search { 38 | padding: 0 12px; 39 | &:hover { 40 | background: transparent; 41 | } 42 | } 43 | .account { 44 | .avatar { 45 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 46 | margin-right: 8px; 47 | color: @primary-color; 48 | vertical-align: top; 49 | background: rgba(255, 255, 255, 0.85); 50 | } 51 | } 52 | } 53 | 54 | .dark { 55 | .action { 56 | color: rgba(255, 255, 255, 0.85); 57 | > span { 58 | color: rgba(255, 255, 255, 0.85); 59 | } 60 | &:hover, 61 | &:global(.opened) { 62 | background: @primary-color; 63 | } 64 | } 65 | } 66 | 67 | :global(.ant-pro-global-header) { 68 | .dark { 69 | .action { 70 | color: @text-color; 71 | > span { 72 | color: @text-color; 73 | } 74 | &:hover { 75 | color: rgba(255, 255, 255, 0.85); 76 | > span { 77 | color: rgba(255, 255, 255, 0.85); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '../../theme.less'; 2 | 3 | .dropdownContainer > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .dropdownContainer { 11 | width: 100% !important; 12 | } 13 | .dropdownContainer > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { DropDownProps } from 'antd/es/dropdown'; 2 | import { Dropdown } from 'antd'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | export type HeaderDropdownProps = { 8 | overlayClassName?: string; 9 | overlay: React.ReactNode | (() => React.ReactNode) | any; 10 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 11 | } & Omit; 12 | 13 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 14 | 15 | ); 16 | 17 | export default HeaderDropdown; 18 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '@ant-design/pro-layout'; 2 | 3 | // loading components from code split 4 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 5 | export default PageLoading; 6 | -------------------------------------------------------------------------------- /src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('lodash'); 2 | const RouterConfig = require('../../config/config').default.routes; 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach((item) => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter((item) => !!item)); 20 | } 21 | 22 | beforeEach(async () => { 23 | await page.goto(`${BASE_URL}`); 24 | await page.evaluate(() => { 25 | localStorage.setItem('authority', '["admin"]'); 26 | }); 27 | }); 28 | 29 | describe('Ant Design Pro E2E test', () => { 30 | const testPage = (path) => async () => { 31 | await page.goto(`${BASE_URL}${path}`); 32 | await page.waitForSelector('footer', { 33 | timeout: 2000, 34 | }); 35 | const haveFooter = await page.evaluate( 36 | () => document.getElementsByTagName('footer').length > 0, 37 | ); 38 | expect(haveFooter).toBeTruthy(); 39 | }; 40 | 41 | const routers = formatter(RouterConfig); 42 | routers.forEach((route) => { 43 | it(`test pages ${route}`, testPage(route)); 44 | }); 45 | 46 | it('topmenu should have footer', async () => { 47 | const params = '?navTheme=light&layout=topmenu'; 48 | await page.goto(`${BASE_URL}${params}`); 49 | await page.waitForSelector('footer', { 50 | timeout: 2000, 51 | }); 52 | const haveFooter = await page.evaluate( 53 | () => document.getElementsByTagName('footer').length > 0, 54 | ); 55 | expect(haveFooter).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/global.tsx: -------------------------------------------------------------------------------- 1 | import { Button, message, notification } from 'antd'; 2 | import defaultSettings from '../config/defaultSettings'; 3 | 4 | const { pwa } = defaultSettings; 5 | const isHttps = document.location.protocol === 'https:'; // if pwa is true 6 | 7 | if (pwa) { 8 | // Notify user if offline now 9 | window.addEventListener('sw.offline', () => { 10 | message.warning('离线'); 11 | }); // Pop up a prompt on the page asking the user if they want to use the latest version 12 | 13 | window.addEventListener('sw.updated', (event: Event) => { 14 | const e = event as CustomEvent; 15 | 16 | const reloadSW = async () => { 17 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 18 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 19 | const worker = e.detail && e.detail.waiting; 20 | 21 | if (!worker) { 22 | return true; 23 | } // Send skip-waiting event to waiting SW with MessageChannel 24 | 25 | await new Promise((resolve, reject) => { 26 | const channel = new MessageChannel(); 27 | 28 | channel.port1.onmessage = (msgEvent) => { 29 | if (msgEvent.data.error) { 30 | reject(msgEvent.data.error); 31 | } else { 32 | resolve(msgEvent.data); 33 | } 34 | }; 35 | 36 | worker.postMessage( 37 | { 38 | type: 'skip-waiting', 39 | }, 40 | [channel.port2], 41 | ); 42 | }); // Refresh current page to use the updated HTML and other assets after SW has skiped waiting 43 | 44 | window.location.reload(true); 45 | return true; 46 | }; 47 | 48 | const key = `open${Date.now()}`; 49 | const btn = ( 50 | 59 | ); 60 | notification.open({ 61 | message: '有新内容', 62 | description: '请点击“刷新”按钮或者手动刷新页面', 63 | btn, 64 | key, 65 | onClose: async () => null, 66 | }); 67 | }); 68 | } else if ('serviceWorker' in navigator && isHttps) { 69 | // unregister service worker 70 | const { serviceWorker } = navigator; 71 | 72 | if (serviceWorker.getRegistrations) { 73 | serviceWorker.getRegistrations().then((sws) => { 74 | sws.forEach((sw) => { 75 | sw.unregister(); 76 | }); 77 | }); 78 | } 79 | 80 | serviceWorker.getRegistration().then((sw) => { 81 | if (sw) sw.unregister(); 82 | }); // remove all caches 83 | // @ts-ignore 84 | 85 | if (window.caches && window.caches.keys) { 86 | caches.keys().then((keys) => { 87 | keys.forEach((key) => { 88 | caches.delete(key); 89 | }); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Inspector } from 'react-dev-inspector'; 3 | import { ConfigProvider } from 'antd'; 4 | import zhCN from 'antd/lib/locale/zh_CN'; 5 | import 'moment/locale/zh-cn'; 6 | 7 | const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; 8 | 9 | const Layout: React.FC = ({ children }) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default Layout; 18 | -------------------------------------------------------------------------------- /src/layouts/LoginLayout.less: -------------------------------------------------------------------------------- 1 | @import '../theme.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .content { 12 | flex: 1; 13 | padding: 160px 0 36px 0; 14 | } 15 | 16 | @media (min-width: @screen-md-min) { 17 | .container { 18 | background-color: #f2f6ff; 19 | } 20 | 21 | .content { 22 | padding: 160px 0 36px 0; 23 | } 24 | } 25 | 26 | .top { 27 | text-align: center; 28 | } 29 | 30 | .header { 31 | height: 48px; 32 | margin-bottom: 16px; 33 | line-height: 48px; 34 | a { 35 | text-decoration: none; 36 | } 37 | } 38 | 39 | .logo { 40 | height: 40px; 41 | vertical-align: center; 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/LoginLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { MenuDataItem } from '@ant-design/pro-layout'; 2 | import { getMenuData, getPageTitle } from '@ant-design/pro-layout'; 3 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 4 | import type { ConnectProps } from 'umi'; 5 | import { connect, Link } from 'umi'; 6 | import React from 'react'; 7 | import type { ConnectState } from '@/models/connect'; 8 | import logo from '../assets/logo_black.svg'; 9 | import styles from './LoginLayout.less'; 10 | 11 | export type UserLayoutProps = { 12 | breadcrumbNameMap: Record; 13 | } & Partial; 14 | 15 | const LoginLayout: React.FC = (props) => { 16 | const { 17 | route = { 18 | routes: [], 19 | }, 20 | } = props; 21 | const { routes = [] } = route; 22 | const { 23 | children, 24 | location = { 25 | pathname: '', 26 | }, 27 | } = props; 28 | const { breadcrumb } = getMenuData(routes); 29 | const title = getPageTitle({ 30 | pathname: location.pathname, 31 | breadcrumb, 32 | ...props, 33 | }); 34 | return ( 35 | 36 | 37 | {title} 38 | 39 | 40 |
41 |
42 |
43 |
44 | 45 | logo 46 | 47 |
48 |
49 | {children} 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default connect(({ settings }: ConnectState) => ({ ...settings }))(LoginLayout); 57 | -------------------------------------------------------------------------------- /src/layouts/StaffAdminSecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {PageLoading} from '@ant-design/pro-layout'; 3 | import type {ConnectProps} from 'umi'; 4 | import {connect, Redirect} from 'umi'; 5 | import {stringify} from 'querystring'; 6 | import type {ConnectState} from '@/models/connect'; 7 | import type {StaffAdminInterface} from '@/services/staffAdmin'; 8 | import {LSExtStaffAdminID} from '../../config/constant'; 9 | 10 | type SecurityLayoutProps = { 11 | loading?: boolean; 12 | currentStaffAdmin?: StaffAdminInterface; 13 | } & ConnectProps; 14 | 15 | type SecurityLayoutState = { 16 | isReady: boolean; 17 | }; 18 | 19 | class StaffAdminSecurityLayout extends React.Component { 20 | state: SecurityLayoutState = { 21 | isReady: false, 22 | }; 23 | 24 | componentDidMount() { 25 | this.setState({ 26 | isReady: true, 27 | }); 28 | const {dispatch} = this.props; 29 | if (dispatch) { 30 | // dispatch({ 31 | // type: 'adminType/changeStatus', 32 | // payload: 'staffAdmin', 33 | // }); 34 | dispatch({ 35 | type: 'staffAdmin/getCurrent', 36 | }); 37 | } 38 | } 39 | 40 | render() { 41 | const {isReady} = this.state; 42 | const {children, loading} = this.props; 43 | // You can replace it to your authentication rule (such as check token exists) 44 | // You can replace it with your own login authentication rules (such as judging whether the token exists) 45 | const isLogin = localStorage.getItem(LSExtStaffAdminID) !== null; 46 | const queryString = stringify({ 47 | redirect: window.location.href, 48 | }); 49 | 50 | if ((!isLogin && loading) || !isReady) { 51 | return ; 52 | } 53 | if (!isLogin && window.location.pathname !== '/staff-admin/login') { 54 | return ; 55 | } 56 | return children; 57 | } 58 | } 59 | 60 | export default connect(({staffAdmin, loading}: ConnectState) => ({ 61 | currentStaffAdmin: staffAdmin.currentStaffAdmin, 62 | loading: loading.models.staffAdmin, 63 | }))(StaffAdminSecurityLayout); 64 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | import pages from './en-US/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.preview.down.block': '下载此页面到本地项目', 15 | 'app.welcome.link.fetch-blocks': '获取全部区块', 16 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 17 | ...globalHeader, 18 | ...menu, 19 | ...settingDrawer, 20 | ...settings, 21 | ...pwa, 22 | ...component, 23 | ...pages, 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页', 6 | 'menu.admin.sub-page': '二级管理页', 7 | 'menu.login': '登录', 8 | 'menu.register': '注册', 9 | 'menu.register.result': '注册结果', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': '分析页', 12 | 'menu.dashboard.monitor': '监控页', 13 | 'menu.dashboard.workplace': '工作台', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': '表单页', 18 | 'menu.form.basic-form': '基础表单', 19 | 'menu.form.step-form': '分步表单', 20 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 21 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 22 | 'menu.form.step-form.result': '分步表单(完成)', 23 | 'menu.form.advanced-form': '高级表单', 24 | 'menu.list': '列表页', 25 | 'menu.list.table-list': '查询表格', 26 | 'menu.list.basic-list': '标准列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(项目)', 31 | 'menu.list.search-list.applications': '搜索列表(应用)', 32 | 'menu.profile': '详情页', 33 | 'menu.profile.basic': '基础详情页', 34 | 'menu.profile.advanced': '高级详情页', 35 | 'menu.result': '结果页', 36 | 'menu.result.success': '成功页', 37 | 'menu.result.fail': '失败页', 38 | 'menu.exception': '异常页', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': '触发错误', 43 | 'menu.account': '个人页', 44 | 'menu.account.center': '个人中心', 45 | 'menu.account.settings': '个人设置', 46 | 'menu.account.trigger': '触发报错', 47 | 'menu.account.logout': '退出登录', 48 | 'menu.editor': '图形编辑器', 49 | 'menu.editor.flow': '流程编辑器', 50 | 'menu.editor.mind': '脑图编辑器', 51 | 'menu.editor.koni': '拓扑编辑器', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | import pages from './en-US/pages'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.preview.down.block': '下载此页面到本地项目', 15 | 'app.welcome.link.fetch-blocks': '获取全部区块', 16 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 17 | ...pages, 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | }; 25 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页', 6 | 'menu.admin.sub-page': '二级管理页', 7 | 'menu.login': '登录', 8 | 'menu.register': '注册', 9 | 'menu.register.result': '注册结果', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': '分析页', 12 | 'menu.dashboard.monitor': '监控页', 13 | 'menu.dashboard.workplace': '工作台', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': '表单页', 18 | 'menu.form.basic-form': '基础表单', 19 | 'menu.form.step-form': '分步表单', 20 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 21 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 22 | 'menu.form.step-form.result': '分步表单(完成)', 23 | 'menu.form.advanced-form': '高级表单', 24 | 'menu.list': '列表页', 25 | 'menu.list.table-list': '查询表格', 26 | 'menu.list.basic-list': '标准列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(项目)', 31 | 'menu.list.search-list.applications': '搜索列表(应用)', 32 | 'menu.profile': '详情页', 33 | 'menu.profile.basic': '基础详情页', 34 | 'menu.profile.advanced': '高级详情页', 35 | 'menu.result': '结果页', 36 | 'menu.result.success': '成功页', 37 | 'menu.result.fail': '失败页', 38 | 'menu.exception': '异常页', 39 | 'menu.exception.not-permission': '403', 40 | 'menu.exception.not-find': '404', 41 | 'menu.exception.server-error': '500', 42 | 'menu.exception.trigger': '触发错误', 43 | 'menu.account': '个人页', 44 | 'menu.account.center': '个人中心', 45 | 'menu.account.settings': '个人设置', 46 | 'menu.account.trigger': '触发报错', 47 | 'menu.account.logout': '退出登录', 48 | 'menu.editor': '图形编辑器', 49 | 'menu.editor.flow': '流程编辑器', 50 | 'menu.editor.mind': '脑图编辑器', 51 | 'menu.editor.koni': '拓扑编辑器', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenSCRM", 3 | "short_name": "OpenSCRM", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/models/connect.d.ts: -------------------------------------------------------------------------------- 1 | import type {MenuDataItem, Settings as ProSettings} from '@ant-design/pro-layout'; 2 | import {GlobalModelState} from './global'; 3 | import type {StaffAdminModelState} from '@/models/staffAdmin'; 4 | 5 | export { GlobalModelState }; 6 | 7 | export type Loading = { 8 | global: boolean; 9 | effects: Record; 10 | models: { 11 | global?: boolean; 12 | menu?: boolean; 13 | setting?: boolean; 14 | staffAdmin?: boolean; 15 | }; 16 | }; 17 | 18 | export type ConnectState = { 19 | global: GlobalModelState; 20 | loading: Loading; 21 | settings: ProSettings; 22 | staffAdmin: StaffAdminModelState; 23 | }; 24 | 25 | export type Route = { 26 | routes?: Route[]; 27 | } & MenuDataItem; 28 | -------------------------------------------------------------------------------- /src/models/setting.ts: -------------------------------------------------------------------------------- 1 | import type { Reducer } from 'umi'; 2 | import type { DefaultSettings } from '../../config/defaultSettings'; 3 | import defaultSettings from '../../config/defaultSettings'; 4 | 5 | export type SettingModelType = { 6 | namespace: 'settings'; 7 | state: DefaultSettings; 8 | reducers: { 9 | changeSetting: Reducer; 10 | }; 11 | }; 12 | 13 | const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => { 14 | const root = document.getElementById('root'); 15 | if (root) { 16 | root.className = colorWeak ? 'colorWeak' : ''; 17 | } 18 | }; 19 | 20 | const SettingModel: SettingModelType = { 21 | namespace: 'settings', 22 | state: defaultSettings, 23 | reducers: { 24 | changeSetting(state = defaultSettings, { payload }) { 25 | const { colorWeak, contentWidth } = payload; 26 | 27 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 28 | window.dispatchEvent(new Event('resize')); 29 | } 30 | updateColorWeak(!!colorWeak); 31 | return { 32 | ...state, 33 | ...payload, 34 | }; 35 | }, 36 | }, 37 | }; 38 | export default SettingModel; 39 | -------------------------------------------------------------------------------- /src/models/staffAdmin.ts: -------------------------------------------------------------------------------- 1 | import type { Effect, Reducer } from 'umi'; 2 | import type { StaffAdminInterface } from '@/services/staffAdmin'; 3 | import { GetCurrentStaffAdmin } from '@/services/staffAdmin'; 4 | import { setAuthority } from '@/utils/authority'; 5 | import { getPageQuery } from '@/utils/utils'; 6 | import { history } from '@@/core/history'; 7 | import { stringify } from 'querystring'; 8 | import { LSExtStaffAdminID } from '../../config/constant'; 9 | 10 | export type StaffAdminModelState = { 11 | currentStaffAdmin?: StaffAdminInterface; 12 | }; 13 | 14 | export type StaffAdminModelType = { 15 | namespace: 'staffAdmin'; 16 | state: StaffAdminModelState; 17 | effects: { 18 | getCurrent: Effect; 19 | }; 20 | reducers: { 21 | applyCurrent: Reducer; 22 | logout: Reducer; 23 | }; 24 | }; 25 | 26 | const StaffAdminModel: StaffAdminModelType = { 27 | namespace: 'staffAdmin', 28 | 29 | state: {}, 30 | 31 | effects: { 32 | *getCurrent(_, { call, put }) { 33 | const response = yield call(GetCurrentStaffAdmin); 34 | yield put({ 35 | type: 'applyCurrent', 36 | payload: response.data, 37 | }); 38 | }, 39 | }, 40 | 41 | reducers: { 42 | applyCurrent(state, action) { 43 | const params = action.payload as StaffAdminInterface; 44 | localStorage.setItem(LSExtStaffAdminID, params.ext_staff_id); 45 | if (params?.role?.permission_ids) { 46 | setAuthority(params.role.permission_ids); 47 | } 48 | return { 49 | currentStaffAdmin: params || {}, 50 | }; 51 | }, 52 | 53 | logout() { 54 | const { redirect } = getPageQuery(); 55 | localStorage.removeItem(LSExtStaffAdminID); 56 | // Note: There may be security issues, please note 57 | if (window.location.pathname !== '/staff-admin/login' && !redirect) { 58 | history.replace({ 59 | pathname: '/staff-admin/login', 60 | search: stringify({ 61 | redirect: window.location.href, 62 | }), 63 | }); 64 | } 65 | 66 | return { 67 | currentStaffAdmin: {}, 68 | }; 69 | }, 70 | }, 71 | }; 72 | 73 | export default StaffAdminModel; 74 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { history } from 'umi'; 4 | 5 | const NoFoundPage: React.FC = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ChatSession/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../../../config/constant'; 3 | 4 | type QueryStaffsParams = Partial<{ 5 | name: string; 6 | page: number; 7 | page_size: number; 8 | role_id: number | string; 9 | sort_field: string; 10 | sort_type: string; 11 | total_rows: number; 12 | type: string; 13 | }> & { 14 | enable_msg_arch: number; 15 | ext_department_ids: number | number[]; 16 | } 17 | 18 | type QueryDepartmentParams = Partial<{ 19 | ext_dept_ids: number[] 20 | ext_parent_id: string; 21 | page: number; 22 | page_size: number; 23 | sort_field: string; 24 | sort_type: string; 25 | total_rows: number; 26 | }> 27 | type QueryChatSessionsParams = Partial<{ 28 | page: number; 29 | page_size: number; 30 | sort_field: string; 31 | sort_type: string; 32 | total_rows: number; 33 | name: string; 34 | }> & { 35 | session_type: string; 36 | ext_staff_id: string; 37 | } 38 | 39 | type QueryChatMessagesParams = Partial<{ 40 | page: number; 41 | page_size: number; 42 | sort_field: string; 43 | sort_type: string; 44 | total_rows: number; 45 | max_id: string; 46 | min_id: string; 47 | limit: number; 48 | send_at_start: string; 49 | send_at_end: string; 50 | }> & { 51 | receiver_id: string; 52 | ext_staff_id: string; 53 | } 54 | 55 | type SearchMessagesParams = { 56 | ext_staff_id: string; 57 | ext_peer_id: string; 58 | keyword: string; 59 | } 60 | 61 | // 获取员工数据 62 | export async function QueryStaffsList(params?: QueryStaffsParams) { 63 | return request(`${StaffAdminApiPrefix}/staffs`, { 64 | params, 65 | }); 66 | } 67 | 68 | // 获取部门列表 69 | export async function QueryDepartmentList(params?: QueryDepartmentParams) { 70 | return request(`${StaffAdminApiPrefix}/departments`, { 71 | params, 72 | }); 73 | } 74 | 75 | // 获取会话列表 76 | export async function QueryChatSessions(params?: QueryChatSessionsParams) { 77 | return request(`${StaffAdminApiPrefix}/customer/chat-sessions`, { 78 | params, 79 | }); 80 | } 81 | 82 | // 获取消息详情 83 | export async function QueryChatMessages(params?: QueryChatMessagesParams) { 84 | return request(`${StaffAdminApiPrefix}/customer/session-msgs`, { 85 | params, 86 | }); 87 | } 88 | 89 | // 搜索消息 90 | export async function SearchMessages(params: SearchMessagesParams) { 91 | return request(`${StaffAdminApiPrefix}/customer/session-msg/action/search`, { 92 | method: 'POST', data: { 93 | ...params, 94 | }, 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Columns/CollapsedStaffs/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .collapsedStaffs { 4 | .staffTagLikeItem { 5 | margin: 4px 4px 2px 0; 6 | padding: 4px 8px; 7 | color: @text-color-secondary; 8 | vertical-align: center; 9 | 10 | .offline { 11 | opacity: 0.5; 12 | } 13 | 14 | .icon { 15 | width: 22px !important; 16 | height: 22px !important; 17 | margin-right: 6px; 18 | border-radius: 4px; 19 | } 20 | 21 | .text { 22 | font-size: 13px; 23 | vertical-align: -2px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Columns/CollapsedStaffs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tag } from 'antd'; 3 | import styles from './index.less'; 4 | import { False } from '../../../../../../config/constant'; 5 | 6 | export interface StaffItem { 7 | online?: number; 8 | id: string; 9 | avatar_url: string; 10 | name: string; 11 | } 12 | 13 | export type CollapsedStaffsProps = { 14 | staffs: StaffItem[] | undefined 15 | limit: number 16 | }; 17 | 18 | const CollapsedStaffs: React.FC = (props) => { 19 | const { staffs, limit } = props; 20 | return ( 21 |
22 | {staffs?.map((staff, index) => { 23 | const len = staffs?.length || 0; 24 | if (index <= limit - 1) { 25 | return ( 26 | 31 | {staff.name} 32 | {staff.name} 33 | 34 | ); 35 | } 36 | 37 | if (index === limit) { 38 | return ( 39 | 43 | +{(len - index)} ... 44 | 45 | ); 46 | } 47 | 48 | return ''; 49 | 50 | })} 51 |
52 | ); 53 | }; 54 | 55 | export default CollapsedStaffs; 56 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Columns/CollapsedTags/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .collapsedTags { 4 | .tagItem { 5 | margin: 4px 4px 2px 0; 6 | padding: 4px 8px; 7 | color: @text-color-secondary; 8 | vertical-align: center; 9 | 10 | .text { 11 | color: @text-color-secondary; 12 | font-size: 13px; 13 | vertical-align: -2px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Columns/CollapsedTags/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tag } from 'antd'; 3 | import styles from './index.less'; 4 | 5 | export interface TagItem { 6 | id: string; 7 | name: string; 8 | tag_name?: string; 9 | } 10 | 11 | export type CollapsedTagsProps = { 12 | tags: TagItem[] | undefined 13 | limit: number 14 | }; 15 | 16 | const CollapsedTags: React.FC = (props) => { 17 | const { tags, limit } = props; 18 | return ( 19 |
20 | {tags?.map((tag, index) => { 21 | const len = tags?.length || 0; 22 | if (index <= limit - 1) { 23 | return ( 24 | 28 | {tag.name || tag.tag_name} 29 | 30 | ); 31 | } 32 | 33 | if (index === limit) { 34 | return ( 35 | 39 | +{(len - index)} ... 40 | 41 | ); 42 | } 43 | 44 | return ''; 45 | 46 | })} 47 |
48 | ); 49 | }; 50 | 51 | export default CollapsedTags; 52 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/CustomerTagSelect/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .selectedItem { 4 | display: -webkit-box; 5 | display: flex; 6 | align-items: center; 7 | color: rgba(0, 0, 0, 0.85); 8 | font-size: 14px; 9 | -webkit-box-align: center; 10 | 11 | .avatar { 12 | margin-right: 4px; 13 | color: rgba(0,0,0,0.65); 14 | } 15 | 16 | span { 17 | color: rgba(0,0,0,0.65); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/EditableTag/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .tagList { 4 | margin-top: 12px; 5 | 6 | @media (max-width: @screen-xs) { 7 | margin-top: 12px; 8 | } 9 | 10 | .tagWrapper { 11 | margin-left: 8px; 12 | 13 | .tagItem { 14 | margin-right: 0; 15 | padding: 5px 16px; 16 | color: @text-color-secondary; 17 | font-size: 14px; 18 | background-color: rgb(248, 248, 248); 19 | border: none; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/EditableTag/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, SetStateAction } from 'react'; 2 | import React, { useState } from 'react'; 3 | import { Badge, Button, Input, Space, Tag } from 'antd'; 4 | import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons'; 5 | import styles from './index.less'; 6 | import _ from 'lodash'; 7 | 8 | export type EditableTagProps = { 9 | tags: string[]; 10 | setTags: Dispatch>; 11 | }; 12 | 13 | const EditableTag: React.FC = (props) => { 14 | const { tags, setTags } = props; 15 | const [inputVisible, setInputVisible] = useState(false); 16 | const [inputValue, setInputValue] = useState(''); 17 | 18 | return ( 19 | <> 20 | 21 | 29 | 30 | {inputVisible && ( 31 | setInputValue(e.currentTarget.value)} 34 | autoFocus={true} 35 | allowClear={true} 36 | placeholder="逗号分隔,回车保存" 37 | onBlur={() => { 38 | setInputValue(''); 39 | setInputVisible(false); 40 | }} 41 | onPressEnter={(e) => { 42 | e.preventDefault(); 43 | const params = inputValue 44 | .replace(',', ',') 45 | .split(',') 46 | .filter((val: any) => val); 47 | setTags(_.uniq([...tags, ...params])); 48 | setInputValue(''); 49 | }} 50 | /> 51 | )} 52 | {tags?.map((tag) => ( 53 | { 59 | setTags(tags.filter((item) => tag !== item)); 60 | }} 61 | style={{ color: 'rgb(199,199,199)' }} 62 | /> 63 | } 64 | > 65 | {tag} 66 | 67 | ))} 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default EditableTag; 74 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/GroupChatSelect/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .selectedItem { 4 | display: -webkit-box; 5 | display: flex; 6 | align-items: center; 7 | color: rgba(0, 0, 0, 0.85); 8 | font-size: 14px; 9 | -webkit-box-align: center; 10 | 11 | .avatar { 12 | margin-right: 4px; 13 | color: rgba(0,0,0,0.65); 14 | } 15 | 16 | .groupName { 17 | width: 100%; 18 | overflow: hidden; 19 | white-space: nowrap; 20 | word-break: keep-all; 21 | text-overflow: ellipsis; 22 | font-size: 14px; 23 | color: rgba(66, 66, 66, 0.85); 24 | } 25 | 26 | .owner { 27 | width: 100%; 28 | overflow: hidden; 29 | white-space: nowrap; 30 | word-break: keep-all; 31 | text-overflow: ellipsis; 32 | font-size: 12px; 33 | color: rgba(66, 66, 66, 0.45); 34 | } 35 | 36 | span { 37 | color: rgba(0,0,0,0.65); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/GroupChatTagSelect/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .selectedItem { 4 | display: -webkit-box; 5 | display: flex; 6 | align-items: center; 7 | color: rgba(0, 0, 0, 0.85); 8 | font-size: 14px; 9 | -webkit-box-align: center; 10 | 11 | .avatar { 12 | margin-right: 4px; 13 | color: rgba(0,0,0,0.65); 14 | } 15 | 16 | span { 17 | color: rgba(0,0,0,0.65); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/ImageUploader/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | 3 | .imageUploader { 4 | :global { 5 | .ant-upload-select-picture-card { 6 | width: auto; 7 | height: auto; 8 | } 9 | } 10 | 11 | .image { 12 | max-width: 260px; 13 | } 14 | 15 | .button { 16 | padding: 40px; 17 | color: rgba(66, 66, 66, 0.65); 18 | 19 | .text { 20 | font-size: 13px; 21 | } 22 | } 23 | 24 | .button:hover { 25 | color: @primary-color !important; 26 | background-color: rgba(@primary-color, 0.05); 27 | } 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Fields/ImageUploader/index.tsx: -------------------------------------------------------------------------------- 1 | import type {Dispatch, SetStateAction} from 'react'; 2 | import React, {useState} from 'react'; 3 | import type {UploadProps} from 'antd'; 4 | import defaultImage from '@/assets/default-image.png' 5 | import {Badge, Image, message, Upload} from 'antd'; 6 | import {CloseCircleFilled, LoadingOutlined, PlusCircleFilled} from '@ant-design/icons'; 7 | import styles from './index.less'; 8 | import _ from 'lodash'; 9 | 10 | export type ImageUploaderProps = { 11 | value?: string, 12 | onChange?: Dispatch>; 13 | } & UploadProps; 14 | 15 | const ImageUploader: React.FC = (props) => { 16 | const {value, onChange} = props; 17 | const [loading, setLoading] = useState(false); 18 | return ( 19 | { 26 | if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) { 27 | message.error('只能上传jpg和png格式'); 28 | return false; 29 | } 30 | if (file.size / 1024 / 1024 > 20) { 31 | message.error('图片最大20M'); 32 | return false; 33 | } 34 | return true; 35 | }} 36 | onChange={(info) => { 37 | if (info.file.status === 'uploading') { 38 | setLoading(true); 39 | return; 40 | } 41 | if (info.file.status === 'done') { 42 | setLoading(false); 43 | } 44 | }} 45 | {...(_.omit(props, ['value']))} 46 | > 47 |
48 | {value && ( 49 | { 53 | e.preventDefault(); 54 | e.stopPropagation(); 55 | if (onChange) { 56 | onChange(''); 57 | } 58 | setLoading(false); 59 | }} 60 | style={{color: 'rgb(199,199,199)'}} 61 | /> 62 | } 63 | > 64 | 70 | 71 | )} 72 | {!value && ( 73 |
74 | {loading ? : } 75 |
上传图片
76 |
77 | )} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default ImageUploader; 84 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Modals/AutoReplyPreviewModal/index.less: -------------------------------------------------------------------------------- 1 | .maskContainer { 2 | z-index: 999; 3 | position: fixed; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | background: #000; 9 | background: rgba(0, 0, 0, .7); 10 | } 11 | 12 | .closeContainer { 13 | position: absolute; 14 | top: -3%; 15 | cursor: pointer; 16 | right: -31px; 17 | border-radius: 50%; 18 | color: #fff; 19 | border: 2px solid #fff; 20 | width: 34px; 21 | height: 34px; 22 | display: -webkit-box; 23 | display: flex; 24 | -webkit-box-align: center; 25 | align-items: center; 26 | -webkit-box-pack: center; 27 | justify-content: center; 28 | } 29 | 30 | .previewContainer { 31 | position: absolute; 32 | top: 50%; 33 | left: 50%; 34 | -webkit-transform: translate(-50%, -50%); 35 | transform: translate(-50%, -50%); 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Modals/AutoReplyPreviewModal/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, SetStateAction } from 'react'; 2 | import React from 'react'; 3 | import styles from './index.less'; 4 | import { CloseOutlined } from '@ant-design/icons'; 5 | import type { WelcomeMsg } from '@/pages/StaffAdmin/CustomerWelcomeMsg/data'; 6 | import AutoReplyPreview from '@/pages/StaffAdmin/Components/Sections/AutoReplyPreview'; 7 | 8 | export type AutoReplyPreviewModalProps = { 9 | visible: boolean; 10 | setVisible: Dispatch>; 11 | autoReply?: WelcomeMsg; 12 | }; 13 | 14 | const AutoReplyPreviewModal: React.FC = (props) => { 15 | const { visible, setVisible, autoReply } = props; 16 | return ( 17 |
setVisible(false)}> 19 |
{ 21 | e.stopPropagation(); 22 | }}> 23 | 24 |
setVisible(false)}> 25 | 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default AutoReplyPreviewModal; 33 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Modals/CustomerTagSelectionModal/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | @import '../../../../../styles/component'; 3 | 4 | .dialog { 5 | :global { 6 | .ant-modal-body { 7 | padding: 24px 40px 0; 8 | } 9 | 10 | .ant-modal-footer { 11 | margin-top: 24px; 12 | padding: 0 40px 36px; 13 | border-top: none; 14 | } 15 | 16 | .ant-modal-body .dialog-title { 17 | width: 100%; 18 | margin-top: 2px; 19 | margin-bottom: 22px; 20 | font-weight: 600; 21 | font-size: 17px; 22 | line-height: 24px; 23 | letter-spacing: 1px; 24 | text-align: center; 25 | } 26 | 27 | .ant-form-item-label { 28 | min-width: 99px; 29 | text-align: right; 30 | @media (max-width: @screen-xs) { 31 | text-align: left; 32 | } 33 | } 34 | } 35 | } 36 | 37 | .tagGroupList { 38 | margin-top: 24px; 39 | overflow-y: auto; 40 | max-height: 468px; 41 | 42 | .tagGroupName { 43 | margin-top: 8px; 44 | font-size: 13px; 45 | line-height: 18px; 46 | color: #999; 47 | white-space: nowrap; 48 | margin-bottom: 5px; 49 | word-break: break-all; 50 | } 51 | 52 | .tagGroupItem { 53 | padding: 4px 0; 54 | 55 | .tagList { 56 | margin-bottom: 6px; 57 | 58 | @media (max-width: @screen-xs) { 59 | margin-top: 6px; 60 | } 61 | 62 | :global { 63 | .tag-item { 64 | color: rgba(0, 0, 0, .65); 65 | padding: 5px 14px; 66 | border: 1px solid #e9e9e9; 67 | border-radius: 4px; 68 | font-size: 14px; 69 | line-height: 18px; 70 | box-sizing: border-box; 71 | height: 28px; 72 | cursor: pointer; 73 | -webkit-user-select: none; 74 | -moz-user-select: none; 75 | -ms-user-select: none; 76 | user-select: none; 77 | background: #f7f7f7; 78 | } 79 | 80 | .selected-tag-item { 81 | background-color: rgb(231, 247, 255); 82 | border-color: @primary-color; 83 | color: @primary-color; 84 | } 85 | } 86 | 87 | .tagItem { 88 | padding: 5px 16px; 89 | color: @text-color-secondary; 90 | font-size: 14px; 91 | } 92 | } 93 | 94 | .groupAction { 95 | display: flex; 96 | flex-wrap: wrap; 97 | margin-top: 12px; 98 | 99 | @media (max-width: @screen-xs) { 100 | margin-top: 12px; 101 | } 102 | 103 | > button { 104 | padding: 2px 6px; 105 | } 106 | } 107 | } 108 | 109 | .tagGroupItem:last-child { 110 | border-bottom: none; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Components/Modals/GroupChatTagSelectionModal/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | @import '../../../../../styles/component'; 3 | 4 | .dialog { 5 | :global { 6 | .ant-modal-body { 7 | padding: 24px 40px 0; 8 | } 9 | 10 | .ant-modal-footer { 11 | margin-top: 24px; 12 | padding: 0 40px 36px; 13 | border-top: none; 14 | } 15 | 16 | .ant-modal-body .dialog-title { 17 | width: 100%; 18 | margin-top: 2px; 19 | margin-bottom: 22px; 20 | font-weight: 600; 21 | font-size: 17px; 22 | line-height: 24px; 23 | letter-spacing: 1px; 24 | text-align: center; 25 | } 26 | 27 | .ant-form-item-label { 28 | min-width: 99px; 29 | text-align: right; 30 | @media (max-width: @screen-xs) { 31 | text-align: left; 32 | } 33 | } 34 | } 35 | } 36 | 37 | .tagGroupList { 38 | margin-top: 24px; 39 | overflow-y: auto; 40 | max-height: 468px; 41 | 42 | .tagGroupName { 43 | margin-top: 8px; 44 | font-size: 13px; 45 | line-height: 18px; 46 | color: #999; 47 | white-space: nowrap; 48 | margin-bottom: 5px; 49 | word-break: break-all; 50 | } 51 | 52 | .tagGroupItem { 53 | padding: 4px 0; 54 | 55 | .tagList { 56 | margin-bottom: 12px; 57 | 58 | @media (max-width: @screen-xs) { 59 | margin-top: 6px; 60 | } 61 | 62 | :global { 63 | .tag-item { 64 | color: rgba(0, 0, 0, .65); 65 | padding: 5px 14px; 66 | border: 1px solid #e9e9e9; 67 | border-radius: 4px; 68 | font-size: 14px; 69 | line-height: 18px; 70 | box-sizing: border-box; 71 | height: 28px; 72 | cursor: pointer; 73 | -webkit-user-select: none; 74 | -moz-user-select: none; 75 | -ms-user-select: none; 76 | user-select: none; 77 | background: #f7f7f7; 78 | } 79 | 80 | .selected-tag-item { 81 | background-color: rgb(231, 247, 255); 82 | border-color: @primary-color; 83 | color: @primary-color; 84 | } 85 | } 86 | 87 | .tagItem { 88 | padding: 5px 16px; 89 | color: @text-color-secondary; 90 | font-size: 14px; 91 | } 92 | } 93 | 94 | .groupAction { 95 | display: flex; 96 | flex-wrap: wrap; 97 | margin-top: 12px; 98 | 99 | @media (max-width: @screen-xs) { 100 | margin-top: 12px; 101 | } 102 | 103 | > button { 104 | padding: 2px 6px; 105 | } 106 | } 107 | } 108 | 109 | .tagGroupItem:last-child { 110 | border-bottom: none; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ContactWay/Components/form.less: -------------------------------------------------------------------------------- 1 | @import '../../../../theme.less'; 2 | 3 | .content { 4 | max-width: 800px; 5 | .formItem { 6 | display: flex; 7 | flex-wrap: wrap; 8 | align-items: center; 9 | justify-content: start; 10 | width: 100%; 11 | margin: 32px 0; 12 | 13 | .label { 14 | position: relative; 15 | //width: 130px; 16 | height: 32px; 17 | color: rgba(0, 0, 0, 0.85); 18 | font-size: 14px; 19 | line-height: 30px; 20 | text-align: right; 21 | vertical-align: middle; 22 | 23 | .required { 24 | &::before { 25 | display: inline-block; 26 | margin-right: 4px; 27 | color: #ff4d4f; 28 | font-size: 14px; 29 | font-family: SimSun, sans-serif; 30 | line-height: 1; 31 | content: '*'; 32 | } 33 | } 34 | } 35 | 36 | :global { 37 | .ant-form-item { 38 | margin-bottom: 0; 39 | } 40 | } 41 | } 42 | 43 | .scheduleList { 44 | margin-top: -12px; 45 | 46 | .tips { 47 | max-width: 680px; 48 | } 49 | 50 | .scheduleItem { 51 | width: 688px; 52 | background-color: rgb(251, 251, 251); 53 | border-color: @border-color-base; 54 | border-style: @border-style-base; 55 | border-width: @border-width-base; 56 | border-radius: @border-radius-base; 57 | .label { 58 | width: 80px; 59 | line-height: 32px; 60 | text-align: right; 61 | } 62 | } 63 | 64 | :global { 65 | .ant-pro-form-list-item, 66 | .ant-pro-form-list-creator-button-bottom { 67 | margin-top: 16px; 68 | } 69 | 70 | .ant-pro-form-list-action { 71 | margin-bottom: 0; 72 | } 73 | 74 | .staff-item { 75 | .container { 76 | background: @white; 77 | border-color: @border-color-base; 78 | border-style: @border-style-base; 79 | border-width: @border-width-base; 80 | border-radius: @border-radius-base; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ContactWay/create.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ContactWay/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { message } from 'antd/es'; 4 | import { history } from 'umi'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type { StaffOption } from '../Components/Modals/StaffTreeSelectionModal'; 8 | import type { SimpleStaffInterface } from '@/services/staff'; 9 | import { QuerySimpleStaffs } from '@/services/staff'; 10 | import type { CommonResp } from '@/services/common'; 11 | import { Create } from '@/pages/StaffAdmin/ContactWay/service'; 12 | import ContactWayForm from '@/pages/StaffAdmin/ContactWay/Components/form'; 13 | import type {CustomerTagGroupItem} from "@/pages/StaffAdmin/CustomerTag/data"; 14 | import {QueryCustomerTagGroups} from "@/services/customer_tag_group"; 15 | 16 | const CreateContactWay: React.FC = () => { 17 | const [allStaffs, setAllStaffs] = useState([]); 18 | const [allTagGroups, setAllTagGroups] = useState([]); 19 | 20 | useEffect(() => { 21 | QueryCustomerTagGroups({page_size: 5000}).then((res) => { 22 | if (res.code === 0) { 23 | setAllTagGroups(res?.data?.items); 24 | } else { 25 | message.error(res.message); 26 | } 27 | }); 28 | }, []); 29 | 30 | useEffect(() => { 31 | QuerySimpleStaffs({ page_size: 5000 }).then((res) => { 32 | if (res.code === 0) { 33 | setAllStaffs( 34 | res?.data?.items?.map((item: SimpleStaffInterface) => { 35 | return { 36 | label: item.name, 37 | value: item.ext_id, 38 | ...item, 39 | }; 40 | }) || [], 41 | ); 42 | } else { 43 | message.error(res.message); 44 | } 45 | }); 46 | }, []); 47 | 48 | return ( 49 | history.goBack()} 51 | backIcon={} 52 | header={{ 53 | title: '创建渠道活码', 54 | }} 55 | > 56 | 57 | { 60 | const params = { ...values }; 61 | const hide = message.loading('处理中'); 62 | const res: CommonResp = await Create(params); 63 | hide(); 64 | if (res.code === 0) { 65 | history.push('/staff-admin/customer-growth/contact-way'); 66 | message.success('添加成功'); 67 | return true; 68 | } 69 | 70 | if (res.message) { 71 | message.error(res.message); 72 | return false; 73 | } 74 | 75 | message.error('添加失败'); 76 | return false; 77 | }} 78 | staffs={allStaffs} 79 | tagGroups={allTagGroups} 80 | /> 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default CreateContactWay; 87 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ContactWay/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { 4 | ContactWayGroupItem, 5 | ContactWayItem, 6 | QueryContactWayGroupParams, 7 | QueryContactWayParams, 8 | } from '@/pages/StaffAdmin/ContactWay/data'; 9 | import type { 10 | CreateContactWayParam, 11 | UpdateContactWayParam, 12 | } from '@/pages/StaffAdmin/ContactWay/data'; 13 | 14 | // 渠道活码分组 15 | export async function QueryGroup(params?: QueryContactWayGroupParams) { 16 | return request(`${StaffAdminApiPrefix}/contact-way-groups`, { 17 | params, 18 | }); 19 | } 20 | 21 | export async function DeleteGroup(params: { ids: string[] }) { 22 | return request(`${StaffAdminApiPrefix}/contact-way-group/action/delete`, { 23 | method: 'POST', 24 | data: { 25 | ...params, 26 | }, 27 | }); 28 | } 29 | 30 | export async function CreateGroup(params: ContactWayGroupItem) { 31 | return request(`${StaffAdminApiPrefix}/contact-way-group`, { 32 | method: 'POST', 33 | data: { 34 | ...params, 35 | }, 36 | }); 37 | } 38 | 39 | export async function UpdateGroup(params: ContactWayGroupItem) { 40 | return request(`${StaffAdminApiPrefix}/contact-way-group/${params.id}`, { 41 | method: 'PUT', 42 | data: { 43 | ...params, 44 | }, 45 | }); 46 | } 47 | 48 | // 获取渠道活码详情 49 | export async function GetDetail(id: string) { 50 | return request(`${StaffAdminApiPrefix}/contact-way/${id}`); 51 | } 52 | 53 | // 渠道活码 54 | export async function Query(params?: QueryContactWayParams) { 55 | return request(`${StaffAdminApiPrefix}/contact-ways`, { 56 | params, 57 | }); 58 | } 59 | 60 | export async function Delete(params: { ids: string[] }) { 61 | return request(`${StaffAdminApiPrefix}/contact-way/action/delete`, { 62 | method: 'POST', 63 | data: { 64 | ...params, 65 | }, 66 | }); 67 | } 68 | 69 | export async function Create(params: CreateContactWayParam) { 70 | return request(`${StaffAdminApiPrefix}/contact-way`, { 71 | method: 'POST', 72 | data: { 73 | ...params, 74 | }, 75 | }); 76 | } 77 | 78 | export async function Update(id: string, params: UpdateContactWayParam) { 79 | return request(`${StaffAdminApiPrefix}/contact-way/${id}`, { 80 | method: 'PUT', 81 | data: { 82 | ...params, 83 | }, 84 | }); 85 | } 86 | 87 | export async function BatchUpdate(params: { ids: string[] } & ContactWayItem) { 88 | return request(`${StaffAdminApiPrefix}/contact-way/action/batch-update`, { 89 | method: 'POST', 90 | data: { 91 | ...params, 92 | }, 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Customer/Components/TableInput.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import { Form, Input,DatePicker} from 'antd'; 3 | import styles from './index.less' 4 | 5 | interface TableInputProps { 6 | name: string; 7 | inputType?: string; 8 | defaultValue?: any; 9 | } 10 | 11 | const TableInput: React.FC = (props) => { 12 | const {defaultValue} = props 13 | const [value] = useState(defaultValue) 14 | 15 | const renderInput = () => { 16 | if(props.name === 'birthday') { 17 | return 18 | } 19 | return 20 | } 21 | 22 | return
23 | 24 | {renderInput()} 25 | 26 |
27 | 28 | } 29 | export default TableInput; 30 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Customer/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .staffTag { 4 | margin: 6px 3px; 5 | padding: 2px 8px; 6 | color: @text-color-secondary; 7 | vertical-align: center; 8 | 9 | .icon { 10 | width: 36px !important; 11 | height: 36px !important; 12 | margin-right: 12px; 13 | font-size: 16px; 14 | border-radius: 4px; 15 | 16 | .svg { 17 | width: 16px; 18 | height: 16px; 19 | } 20 | } 21 | 22 | .text_corp { 23 | font-size: 14px; 24 | vertical-align: -1px; 25 | color: orange; 26 | } 27 | 28 | .text { 29 | color: @text-color-secondary; 30 | font-size: 14px; 31 | vertical-align: -1px; 32 | } 33 | 34 | } 35 | 36 | // select组件,员工选项 37 | .staffOption { 38 | position: relative; 39 | color: #333; 40 | font-size: 14px; 41 | 42 | .avatar { 43 | position: relative; 44 | width: 26px; 45 | height: 26px; 46 | overflow: hidden; 47 | font-size: 16px; 48 | border-radius: 2px; 49 | } 50 | 51 | .text { 52 | margin-left: 6px; 53 | } 54 | } 55 | 56 | .tagContainer { 57 | max-width: 700px; 58 | :global { 59 | .tag-item { 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | min-width: 40px; 64 | height: 32px; 65 | text-align: center; 66 | color: @text-color-secondary; 67 | padding: 4px 8px; 68 | font-size: 13px; 69 | margin-right: 4px; 70 | text-align: center; 71 | background-color: rgb(241, 250, 255); 72 | border: 1px solid rgb(241, 250, 255); 73 | } 74 | } 75 | } 76 | 77 | .survey{ 78 | width: 100%; 79 | display: flex; 80 | justify-content: space-between; 81 | .cusSurveyLeft{ 82 | width:80%; 83 | } 84 | .cusSurveyRight{ 85 | width: 14%; 86 | margin-right: 50px; 87 | .eventsTitle{ 88 | display: flex; 89 | justify-content: space-between; 90 | margin-bottom: 20px; 91 | .titleText{ 92 | color:rgba(0, 0, 0, 0.85); 93 | font-weight: bold; 94 | font-size: 16px; 95 | } 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerLoss/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { CommonQueryParams, Pager } from '@/services/common'; 2 | 3 | export type CustomerLossItem = Partial<{ 4 | id: string; 5 | ext_customer_id: string; 6 | customer_avatar: string; 7 | customer_corp_name: string; 8 | customer_type: number; 9 | ext_customer_name: string; 10 | relation_create_at: Date; 11 | customer_delete_staff_at: Date; 12 | staff_avatar: string; 13 | staff_name: string; 14 | staff_id: number; 15 | in_connection_time_range: number; 16 | tags: any[]; 17 | }>; 18 | 19 | export type CustomerLossListData = { 20 | items: CustomerLossItem[]; 21 | pager: Partial; 22 | }; 23 | 24 | export type QueryCustomerLossParams = { 25 | ext_department_id?: number; 26 | ext_staff_id?: string; 27 | loss_start?: Date; 28 | loss_end?: Date; 29 | connection_create_start?: Date; 30 | connection_create_end?: Date; 31 | time_span_lower_limit?: number; 32 | } & CommonQueryParams; 33 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerLoss/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .staffTag { 4 | margin: 6px 3px; 5 | padding: 2px 8px; 6 | color: @text-color-secondary; 7 | vertical-align: center; 8 | 9 | .icon { 10 | width: 36px !important; 11 | height: 36px !important; 12 | margin-right: 12px; 13 | font-size: 16px; 14 | border-radius: 4px; 15 | 16 | svg { 17 | width: 16px; 18 | height: 16px; 19 | } 20 | } 21 | 22 | .text { 23 | color: @text-color-secondary; 24 | font-size: 14px; 25 | vertical-align: -1px; 26 | } 27 | } 28 | 29 | // select组件,员工选项 30 | .staffOption { 31 | position: relative; 32 | color: #333; 33 | font-size: 14px; 34 | 35 | .avatar { 36 | position: relative; 37 | width: 26px; 38 | height: 26px; 39 | overflow: hidden; 40 | font-size: 16px; 41 | border-radius: 2px; 42 | } 43 | 44 | .text { 45 | margin-left: 6px; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerLoss/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { QueryCustomerLossParams } from '@/pages/StaffAdmin/CustomerLoss/data'; 4 | 5 | // 查询流失提醒记录 6 | export async function QueryCustomerLoss(params?: QueryCustomerLossParams) { 7 | return request(`${StaffAdminApiPrefix}/customer/losses`, { 8 | params, 9 | }); 10 | } 11 | 12 | // 导出流失提醒记录 13 | export async function ExportCustomerLoss(params?: QueryCustomerLossParams) { 14 | return request(`${StaffAdminApiPrefix}/customer/action/customers-losses-data-export`, { 15 | responseType: 'blob', 16 | params, 17 | }); 18 | } 19 | 20 | export interface CustomerLossNotifyRuleInterface { 21 | is_notify_staff: number; 22 | } 23 | 24 | export async function GetCustomerLossNotifyRule() { 25 | return request(`${StaffAdminApiPrefix}/customer/action/get-loss-notify-rule`); 26 | } 27 | 28 | export async function UpdateCustomerLossNotifyRule(params: CustomerLossNotifyRuleInterface) { 29 | return request(`${StaffAdminApiPrefix}/customer/action/update-loss-notify-rule`, { 30 | method: 'POST', 31 | data: { 32 | ...params, 33 | }, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/Components/form.less: -------------------------------------------------------------------------------- 1 | @import '../../../../theme.less'; 2 | 3 | .content { 4 | max-width: 800px; 5 | 6 | .formItem { 7 | display: flex; 8 | flex-wrap: wrap; 9 | align-items: center; 10 | justify-content: start; 11 | width: 100%; 12 | margin: 32px 0; 13 | 14 | .label { 15 | position: relative; 16 | //width: 130px; 17 | height: 32px; 18 | color: rgba(0, 0, 0, 0.85); 19 | font-size: 14px; 20 | line-height: 30px; 21 | text-align: right; 22 | vertical-align: middle; 23 | 24 | .required { 25 | &::before { 26 | display: inline-block; 27 | margin-right: 4px; 28 | color: #ff4d4f; 29 | font-size: 14px; 30 | font-family: SimSun, sans-serif; 31 | line-height: 1; 32 | content: '*'; 33 | } 34 | } 35 | } 36 | 37 | :global { 38 | .ant-form-item { 39 | margin-bottom: 0; 40 | } 41 | } 42 | } 43 | } 44 | 45 | .multiFormItemSection { 46 | background: #fbfbfb; 47 | border-radius: 2px; 48 | border: 1px solid #ebebeb; 49 | padding: 13px 20px; 50 | margin-top: -12px; 51 | margin-bottom: 20px; 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { message } from 'antd/es'; 4 | import { history } from 'umi'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type { CommonResp } from '@/services/common'; 8 | import { Create } from '@/pages/StaffAdmin/CustomerMassMsg/service'; 9 | import CustomerMassMsgForm from '@/pages/StaffAdmin/CustomerMassMsg/Components/form'; 10 | import type { CustomerMassMsgItem } from '@/pages/StaffAdmin/CustomerMassMsg/data'; 11 | import type { FormInstance } from 'antd'; 12 | 13 | const CreateCustomerMassMsg: React.FC = () => { 14 | const [currentCustomerMassMsg] = useState(); 15 | const formRef = useRef(); 16 | 17 | return ( 18 | history.goBack()} 20 | backIcon={} 21 | header={{ 22 | title: '创建群发', 23 | }} 24 | > 25 | 26 | { 30 | const params = { ...values }; 31 | const hide = message.loading('处理中'); 32 | const res: CommonResp = await Create(params); 33 | hide(); 34 | if (res.code === 0) { 35 | history.push('/staff-admin/customer-conversion/customer-mass-msg'); 36 | message.success('添加成功'); 37 | return true; 38 | } 39 | 40 | if (res.message) { 41 | message.error(res.message); 42 | return false; 43 | } 44 | 45 | message.error('添加失败'); 46 | return false; 47 | }} 48 | initialValues={currentCustomerMassMsg} 49 | /> 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default CreateCustomerMassMsg; 56 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { CommonQueryParams } from '@/services/common'; 2 | 3 | export type MsgType = 'image' | 'link' | 'miniprogram' | 'video'; 4 | 5 | export interface Image { 6 | title: string; 7 | media_id?: string; 8 | pic_url: string; 9 | } 10 | 11 | export interface Link { 12 | title: string; 13 | url: string; 14 | picurl?: string; 15 | desc?: string; 16 | } 17 | 18 | export interface Video { 19 | title: string; 20 | media_id: string; 21 | } 22 | 23 | export interface Miniprogram { 24 | title: string; 25 | pic_media_id: string; 26 | app_id: string; 27 | page: string; 28 | } 29 | 30 | 31 | export interface Attachment { 32 | id: string; 33 | msgtype: MsgType; 34 | name: string; 35 | image?: Image; 36 | link?: Link; 37 | video?: Video; 38 | miniprogram?: Miniprogram; 39 | } 40 | 41 | export interface Msg { 42 | text: string; 43 | attachments?: Attachment[]; 44 | } 45 | 46 | export interface ExtCustomerFilter { 47 | ext_staff_ids?: string[]; 48 | ext_customer_ids?: string[]; 49 | ext_group_chat_ids?: string[]; 50 | gender?: number; 51 | ext_department_ids?: any; 52 | ext_tag_ids?: string[]; 53 | tag_logical_condition?: any; 54 | exclude_ext_tag_ids?: string[]; 55 | start_time?: string; 56 | end_time?: string; 57 | } 58 | 59 | export interface Staff { 60 | id: string; 61 | avatar_url: string; 62 | ext_staff_id: string; 63 | name: string; 64 | } 65 | 66 | export interface CustomerMassMsgItem { 67 | id: string; 68 | ext_corp_id: string; 69 | ext_creator_id: string; 70 | send_type: number; 71 | exd_staff_ids: string[]; 72 | ext_department_ids?: any; 73 | msg: Msg; 74 | ext_msg_id: string; 75 | mission_status: number; 76 | ext_customer_filter_enable: number; 77 | ext_customer_filter: ExtCustomerFilter; 78 | delivered_num: number; 79 | success_num: number; 80 | undelivered_num: number; 81 | failed_num: number; 82 | staffs?: Staff[]; 83 | created_at: Date; 84 | updated_at: Date; 85 | deleted_at?: any; 86 | } 87 | 88 | export interface CreateCustomerMassMsgParam { 89 | id?: string; 90 | ext_customer_filter_enable: 1 | 2; 91 | chat_type: string; 92 | ext_staff_ids: string[]; 93 | msg: Msg; 94 | ext_customer_filter?: ExtCustomerFilter; 95 | send_at: string; 96 | send_type: number; 97 | ext_department_ids: number[]; 98 | } 99 | 100 | export type QueryCustomerMassMsgParam = CommonQueryParams; 101 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/detail.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .detailContainer { 4 | .previewContainer { 5 | .title { 6 | width: 274px; 7 | text-align: center; 8 | display: block; 9 | } 10 | } 11 | 12 | :global { 13 | .ant-descriptions-item { 14 | padding-bottom: 26px !important; 15 | } 16 | 17 | .ant-descriptions-item-label { 18 | color: rgba(66, 66, 66, 0.8); 19 | } 20 | 21 | .ant-descriptions-bordered .ant-descriptions-view { 22 | border: 1px solid #f0f0f0; 23 | } 24 | 25 | .ant-descriptions-bordered .ant-descriptions-view > table { 26 | table-layout: auto; 27 | } 28 | 29 | .ant-descriptions-bordered .ant-descriptions-item-label, 30 | .ant-descriptions-bordered .ant-descriptions-item-content { 31 | padding: 16px 24px; 32 | border-right: 1px solid #f0f0f0; 33 | } 34 | 35 | .ant-descriptions-bordered .ant-descriptions-item-label:last-child, 36 | .ant-descriptions-bordered .ant-descriptions-item-content:last-child { 37 | border-right: none; 38 | } 39 | 40 | .ant-descriptions-bordered .ant-descriptions-item-label { 41 | background-color: #fafafa; 42 | } 43 | 44 | .ant-descriptions-bordered .ant-descriptions-item-label::after { 45 | display: none; 46 | } 47 | 48 | .ant-descriptions-bordered .ant-descriptions-row { 49 | border-bottom: 1px solid #f0f0f0; 50 | } 51 | 52 | .ant-descriptions-bordered .ant-descriptions-row:last-child { 53 | border-bottom: none; 54 | } 55 | 56 | .ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-label, 57 | .ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-content { 58 | padding: 12px 24px; 59 | } 60 | 61 | .ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-label, 62 | .ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-content { 63 | padding: 8px 16px; 64 | } 65 | 66 | .ant-descriptions-rtl { 67 | direction: rtl; 68 | } 69 | 70 | .ant-descriptions-rtl .ant-descriptions-item-label::after { 71 | margin: 0 2px 0 8px; 72 | } 73 | 74 | .ant-descriptions-rtl.ant-descriptions-bordered .ant-descriptions-item-label, 75 | .ant-descriptions-rtl.ant-descriptions-bordered .ant-descriptions-item-content { 76 | border-right: none; 77 | border-left: 1px solid #f0f0f0; 78 | } 79 | 80 | .ant-descriptions-rtl.ant-descriptions-bordered .ant-descriptions-item-label:last-child, 81 | .ant-descriptions-rtl.ant-descriptions-bordered .ant-descriptions-item-content:last-child { 82 | border-left: none; 83 | } 84 | 85 | .ant-descriptions-row > th, 86 | .ant-descriptions-row > td { 87 | padding-bottom: 16px; 88 | } 89 | 90 | .ant-descriptions-row:last-child { 91 | border-bottom: none; 92 | } 93 | 94 | 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/edit.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import {PageContainer} from '@ant-design/pro-layout'; 3 | import {message} from 'antd/es'; 4 | import {history} from 'umi'; 5 | import {LeftOutlined} from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type {CommonResp} from '@/services/common'; 8 | import {Get, Update} from '@/pages/StaffAdmin/CustomerMassMsg/service'; 9 | import CustomerMassMsgForm from '@/pages/StaffAdmin/CustomerMassMsg/Components/form'; 10 | import type {CustomerMassMsgItem} from '@/pages/StaffAdmin/CustomerMassMsg/data'; 11 | import type {FormInstance} from 'antd'; 12 | 13 | const EditCustomerMassMsg: React.FC = () => { 14 | const [currentCustomerMassMsg, setCurrentCustomerMassMsg] = useState(); 15 | const formRef = useRef(); 16 | const id = new URLSearchParams(window.location.search).get('id'); 17 | 18 | useEffect(() => { 19 | if (id) { 20 | const hide = message.loading('加载数据中'); 21 | Get(id).then((res) => { 22 | hide(); 23 | if (res.code === 0) { 24 | setCurrentCustomerMassMsg(res.data); 25 | formRef.current?.setFieldsValue(res.data); 26 | } else { 27 | message.error(res.message); 28 | } 29 | }); 30 | } 31 | }, []); 32 | 33 | return ( 34 | history.goBack()} 36 | backIcon={} 37 | header={{ 38 | title: '修改群发', 39 | }} 40 | > 41 | 42 | { 46 | console.log("values", values); 47 | const params = {...values}; 48 | const hide = message.loading('处理中'); 49 | const res: CommonResp = await Update(params); 50 | hide(); 51 | if (res.code === 0) { 52 | history.push('/staff-admin/customer-conversion/customer-mass-msg'); 53 | message.success('修改成功'); 54 | return true; 55 | } 56 | 57 | if (res.message) { 58 | message.error(res.message); 59 | return false; 60 | } 61 | 62 | message.error('修改失败'); 63 | return false; 64 | }} 65 | initialValues={currentCustomerMassMsg} 66 | itemID={id || ''} 67 | /> 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default EditCustomerMassMsg; 74 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerMassMsg/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { CreateCustomerMassMsgParam, QueryCustomerMassMsgParam } from '@/pages/StaffAdmin/CustomerMassMsg/data'; 4 | import type { RcFile } from 'rc-upload/lib/interface'; 5 | 6 | export async function Query(params?: QueryCustomerMassMsgParam) { 7 | return request(`${StaffAdminApiPrefix}/customer/mass-msgs`, { 8 | params: { 9 | ...params, 10 | }, 11 | }); 12 | } 13 | 14 | export async function Create(params: CreateCustomerMassMsgParam) { 15 | return request(`${StaffAdminApiPrefix}/customer/mass-msg`, { 16 | method: 'POST', 17 | data: { 18 | ...params, 19 | }, 20 | }); 21 | } 22 | 23 | export async function Update(params: CreateCustomerMassMsgParam) { 24 | return request(`${StaffAdminApiPrefix}/customer/mass-msg/${params.id}`, { 25 | method: 'PUT', 26 | data: { 27 | ...params, 28 | }, 29 | }); 30 | } 31 | 32 | export async function Get(id: string) { 33 | return request(`${StaffAdminApiPrefix}/customer/mass-msg/${id}`, { 34 | method: 'GET', 35 | }); 36 | } 37 | 38 | export async function Delete(params: { ids: string[] }) { 39 | return request(`${StaffAdminApiPrefix}/customer/mass-msg/action/delete`, { 40 | method: 'POST', 41 | data: { 42 | ...params, 43 | }, 44 | }); 45 | } 46 | 47 | export async function Notify(params: { ids: string[] }) { 48 | return request(`${StaffAdminApiPrefix}/customer/mass-msg/action/notify`, { 49 | method: 'POST', 50 | data: { 51 | ...params, 52 | }, 53 | }); 54 | } 55 | 56 | export async function UploadImage(filename: string, file: RcFile) { 57 | return fetch(`${StaffAdminApiPrefix}/customer/mass-msg/action/upload-image?filename=${filename}`, { 58 | method: 'POST', 59 | // headers: { 60 | // "Content-Type": "You will perhaps need to define a content-type here" 61 | // }, 62 | body: file 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerTag/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { CommonQueryParams, Pager } from '@/services/common'; 2 | 3 | export type CustomerTagGroupItem = Partial<{ 4 | id: string; 5 | ext_corp_id: string; 6 | ext_creator_id: string; 7 | ext_id: string; 8 | name: string; 9 | create_time: number; 10 | order: number; 11 | department_list?: number[]; 12 | tags: CustomerTag[]; 13 | created_at: Date; 14 | updated_at: Date; 15 | deleted_at?: any; 16 | 17 | remove_ext_tag_ids?: string[]; 18 | }>; 19 | 20 | export type CreateCustomerTagParam = Partial & { 21 | ext_tag_group_id: string; 22 | names: string[]; 23 | }; 24 | 25 | export type CreateCustomerTagGroupParam = Partial<{ 26 | department_list: number[]; 27 | tag_name: string[]; 28 | name: string; 29 | }>; 30 | 31 | export type CustomerTag = Partial<{ 32 | id: string; 33 | ext_corp_id: string; 34 | ext_creator_id: string; 35 | ext_tag_id: string; 36 | ext_id: string; 37 | group_name: string; 38 | name: string; 39 | create_time: number; 40 | order: number; 41 | type: number; 42 | created_at: Date; 43 | updated_at: Date; 44 | deleted_at?: any; 45 | }>; 46 | 47 | export type CustomerTagGroupListData = { 48 | items: CustomerTagGroupItem[]; 49 | pager: Partial; 50 | }; 51 | 52 | export type QueryCustomerTagGroupParams = { 53 | ext_department_ids?: number[]; 54 | name?: string; 55 | } & CommonQueryParams; 56 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerTag/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .queryFilter { 4 | :global{ 5 | .ant-pro-card-body { 6 | padding-bottom: 0; 7 | } 8 | } 9 | } 10 | 11 | .tagGroupList { 12 | .tagGroupItem { 13 | padding: 32px 0; 14 | border-bottom: 1px dashed #e8e8e8; 15 | 16 | .tagName { 17 | margin-top: 12px; 18 | 19 | > h4 { 20 | display: flex; 21 | flex-wrap: wrap; 22 | color: #222; 23 | font-weight: 600 !important; 24 | word-break: break-all; 25 | } 26 | } 27 | 28 | .tagList { 29 | margin-top: 12px; 30 | 31 | @media (max-width: @screen-xs) { 32 | margin-top: 12px; 33 | } 34 | 35 | .tagItem { 36 | padding: 5px 16px; 37 | color: @text-color-secondary; 38 | font-size: 14px; 39 | } 40 | } 41 | 42 | .groupAction { 43 | display: flex; 44 | flex-wrap: wrap; 45 | margin-top: 12px; 46 | 47 | @media (max-width: @screen-xs) { 48 | margin-top: 12px; 49 | } 50 | 51 | > button { 52 | padding: 2px 6px; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerTag/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { 4 | CreateCustomerTagParam, 5 | CustomerTagGroupItem, 6 | QueryCustomerTagGroupParams, 7 | } from '@/pages/StaffAdmin/CustomerTag/data'; 8 | 9 | // 查询流失提醒记录 10 | export async function Query(params?: QueryCustomerTagGroupParams) { 11 | return request(`${StaffAdminApiPrefix}/customer/tag-groups`, { 12 | params, 13 | }); 14 | } 15 | 16 | export async function Delete(params: { ext_ids: string[] }) { 17 | return request(`${StaffAdminApiPrefix}/customer/tag-group/action/delete`, { 18 | method: 'POST', 19 | data: { 20 | ...params, 21 | }, 22 | }); 23 | } 24 | 25 | export async function Create(params: CustomerTagGroupItem) { 26 | return request(`${StaffAdminApiPrefix}/customer/tag-group`, { 27 | method: 'POST', 28 | data: { 29 | ...params, 30 | }, 31 | }); 32 | } 33 | 34 | export async function CreateTag(params: CreateCustomerTagParam) { 35 | return request(`${StaffAdminApiPrefix}/customer/tag`, { 36 | method: 'POST', 37 | data: { 38 | ...params, 39 | }, 40 | }); 41 | } 42 | 43 | export async function Sync() { 44 | return request(`${StaffAdminApiPrefix}/customer/tag/action/sync`, { 45 | method: 'POST', 46 | }); 47 | } 48 | 49 | export async function Update( 50 | params: CustomerTagGroupItem & { ext_id: string; remove_ext_tag_ids: string[] }, 51 | ) { 52 | return request(`${StaffAdminApiPrefix}/customer/tag-group/${params.ext_id}`, { 53 | method: 'PUT', 54 | data: { 55 | ...params, 56 | }, 57 | }); 58 | } 59 | 60 | // 交换标签组排序权重 61 | export async function ExchangeOrder(params: { id: string; exchange_order_id: string }) { 62 | return request(`${StaffAdminApiPrefix}/customer/tag-group/action/exchange-order`, { 63 | method: 'POST', 64 | data: { 65 | ...params, 66 | }, 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerWelcomeMsg/Components/form.less: -------------------------------------------------------------------------------- 1 | @import '../../../../theme.less'; 2 | 3 | .content { 4 | max-width: 800px; 5 | .formItem { 6 | display: flex; 7 | flex-wrap: wrap; 8 | align-items: center; 9 | justify-content: start; 10 | width: 100%; 11 | margin: 32px 0; 12 | 13 | .label { 14 | position: relative; 15 | //width: 130px; 16 | height: 32px; 17 | color: rgba(0, 0, 0, 0.85); 18 | font-size: 14px; 19 | line-height: 30px; 20 | text-align: right; 21 | vertical-align: middle; 22 | 23 | .required { 24 | &::before { 25 | display: inline-block; 26 | margin-right: 4px; 27 | color: #ff4d4f; 28 | font-size: 14px; 29 | font-family: SimSun, sans-serif; 30 | line-height: 1; 31 | content: '*'; 32 | } 33 | } 34 | } 35 | 36 | :global { 37 | .ant-form-item { 38 | margin-bottom: 0; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerWelcomeMsg/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { message } from 'antd/es'; 4 | import { history } from 'umi'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type { CommonResp } from '@/services/common'; 8 | import { Create } from '@/pages/StaffAdmin/CustomerWelcomeMsg/service'; 9 | import CustomerWelcomeMsgForm from '@/pages/StaffAdmin/CustomerWelcomeMsg/Components/form'; 10 | import type { CustomerWelcomeMsgItem } from '@/pages/StaffAdmin/CustomerWelcomeMsg/data'; 11 | import type { FormInstance } from 'antd'; 12 | import type { StaffOption } from '@/pages/StaffAdmin/Components/Modals/StaffTreeSelectionModal'; 13 | import { QuerySimpleStaffs } from '@/services/staff'; 14 | 15 | const CreateCustomerWelcomeMsg: React.FC = () => { 16 | const [currentCustomerWelcomeMsg] = useState(); 17 | const formRef = useRef(); 18 | const [allStaffs, setAllStaffs] = useState([]); 19 | 20 | useEffect(() => { 21 | QuerySimpleStaffs({ page_size: 5000 }).then((res) => { 22 | if (res.code === 0) { 23 | setAllStaffs(res?.data?.items || []); 24 | } else { 25 | message.error(res.message); 26 | } 27 | }); 28 | }, []); 29 | 30 | return ( 31 | history.goBack()} 33 | backIcon={} 34 | header={{ 35 | title: '创建欢迎语', 36 | }} 37 | > 38 | 39 | { 44 | const params = { ...values }; 45 | const hide = message.loading('处理中'); 46 | const res: CommonResp = await Create(params); 47 | hide(); 48 | if (res.code === 0) { 49 | history.push('/staff-admin/customer-conversion/customer-welcome-msg'); 50 | message.success('添加成功'); 51 | return true; 52 | } 53 | 54 | if (res.message) { 55 | message.error(res.message); 56 | return false; 57 | } 58 | 59 | message.error('添加失败'); 60 | return false; 61 | }} 62 | initialValues={currentCustomerWelcomeMsg} 63 | /> 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default CreateCustomerWelcomeMsg; 70 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerWelcomeMsg/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { CommonQueryParams } from '@/services/common'; 2 | 3 | export interface Image { 4 | title: string; 5 | media_id?: string; 6 | pic_url: string; 7 | } 8 | 9 | export interface Link { 10 | title: string; 11 | url: string; 12 | picurl?: string; 13 | desc?: string; 14 | } 15 | 16 | export interface Video { 17 | title: string; 18 | media_id: string; 19 | } 20 | 21 | export interface Miniprogram { 22 | title: string; 23 | pic_media_id: string; 24 | app_id: string; 25 | page: string; 26 | } 27 | 28 | export type MsgType = 'image' | 'link' | 'miniprogram' | 'video'; 29 | 30 | export interface Attachment { 31 | id: string; 32 | msgtype: MsgType; 33 | name: string; 34 | image?: Image; 35 | link?: Link; 36 | video?: Video; 37 | miniprogram?: Miniprogram; 38 | } 39 | 40 | export interface WelcomeMsg { 41 | text: string; 42 | attachments?: Attachment[]; 43 | } 44 | 45 | export interface TimePeriodMsg { 46 | id: string; 47 | ext_corp_id: string; 48 | ext_creator_id: string; 49 | welcome_msg: WelcomeMsg2; 50 | main_welcome_msg_id: string; 51 | enable_time_period_msg: number; 52 | time_period_msg?: any; 53 | effective_at: number[]; 54 | start_time: string; 55 | end_time: string; 56 | created_at: Date; 57 | updated_at: Date; 58 | deleted_at?: any; 59 | } 60 | 61 | export interface Department { 62 | id: string; 63 | ext_id: number; 64 | name: string; 65 | } 66 | 67 | export interface Staff { 68 | id: string; 69 | avatar_url: string; 70 | ext_staff_id: string; 71 | name: string; 72 | } 73 | 74 | export interface CustomerWelcomeMsgItem { 75 | id: string; 76 | name: string; 77 | ext_corp_id: string; 78 | ext_creator_id: string; 79 | welcome_msg: WelcomeMsg; 80 | main_welcome_msg_id?: any; 81 | enable_time_period_msg: number; 82 | time_period_msg: TimePeriodMsg[]; 83 | effective_at?: any; 84 | start_time: string; 85 | end_time: string; 86 | created_at: Date; 87 | updated_at: Date; 88 | deleted_at?: any; 89 | department: Department[]; 90 | staffs: Staff[]; 91 | } 92 | 93 | 94 | export type CreateCustomerWelcomeMsgParam = { 95 | name: string; 96 | welcome_msg: WelcomeMsg; 97 | ext_staff_ids: string[]; 98 | enable_time_period_msg: number; 99 | ext_department_ids: number[]; 100 | time_period_msg: TimePeriodMsg[]; 101 | }; 102 | 103 | export type UpdateCustomerWelcomeMsgParam = Partial<{ 104 | id: string; 105 | name: string; 106 | welcome_msg: WelcomeMsg; 107 | ext_staff_ids: string[]; 108 | enable_time_period_msg: number; 109 | ext_department_ids: number[]; 110 | time_period_msg: TimePeriodMsg[]; 111 | }>; 112 | 113 | export type QueryCustomerWelcomeMsgParam = CommonQueryParams; 114 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/CustomerWelcomeMsg/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../../../config/constant'; 3 | import type { 4 | CreateCustomerWelcomeMsgParam, 5 | QueryCustomerWelcomeMsgParam, 6 | UpdateCustomerWelcomeMsgParam, 7 | } from '@/pages/StaffAdmin/CustomerWelcomeMsg/data'; 8 | 9 | export async function Query(params?: QueryCustomerWelcomeMsgParam) { 10 | return request(`${StaffAdminApiPrefix}/customer/welcome-msgs`, { 11 | params: { 12 | ...params, 13 | }, 14 | }); 15 | } 16 | 17 | export async function Create(params: CreateCustomerWelcomeMsgParam) { 18 | return request(`${StaffAdminApiPrefix}/customer/welcome-msg`, { 19 | method: 'POST', 20 | data: { 21 | ...params, 22 | }, 23 | }); 24 | } 25 | 26 | export async function Update(params: UpdateCustomerWelcomeMsgParam) { 27 | return request(`${StaffAdminApiPrefix}/customer/welcome-msg/${params.id}`, { 28 | method: 'PUT', 29 | data: { 30 | ...params, 31 | }, 32 | }); 33 | } 34 | 35 | export async function Delete(params: { ids: string[] }) { 36 | return request(`${StaffAdminApiPrefix}/customer/welcome-msg/action/delete`, { 37 | method: 'POST', 38 | data: { 39 | ...params, 40 | }, 41 | }); 42 | } 43 | 44 | 45 | export async function Get(id: string) { 46 | return request(`${StaffAdminApiPrefix}/customer/welcome-msg/${id}`, { 47 | method: 'GET', 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/DeleteCustomerRecord/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { CommonQueryParams, Pager } from '@/services/common'; 2 | 3 | export type DeleteCustomerRecordItem = Partial<{ 4 | id: string; 5 | ext_customer_id: string; 6 | ext_customer_avatar: string; 7 | ext_customer_name: string; 8 | customer_corp_name: string; 9 | customer_type: number; 10 | relation_create_at: Date; 11 | relation_delete_at: Date; 12 | ext_staff_avatar: string; 13 | ext_staff_id: string; 14 | staff_id: number; 15 | staff_name: string; 16 | }>; 17 | 18 | export type DeleteCustomerRecordListData = { 19 | items: DeleteCustomerRecordItem[]; 20 | pager: Partial; 21 | }; 22 | 23 | export type QueryDeleteCustomerRecordParams = { 24 | ext_department_id?: number; 25 | ext_staff_id?: string; 26 | connection_create_start?: Date; 27 | connection_create_end?: Date; 28 | delete_customer_start?: Date; 29 | delete_customer_end?: Date; 30 | } & CommonQueryParams; 31 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/DeleteCustomerRecord/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .staffTag { 4 | margin: 6px 3px; 5 | padding: 2px 8px; 6 | color: @text-color-secondary; 7 | vertical-align: center; 8 | 9 | .icon { 10 | width: 36px !important; 11 | height: 36px !important; 12 | margin-right: 12px; 13 | font-size: 16px; 14 | border-radius: 4px; 15 | 16 | svg { 17 | width: 16px; 18 | height: 16px; 19 | } 20 | } 21 | 22 | .text { 23 | color: @text-color-secondary; 24 | font-size: 14px; 25 | vertical-align: -1px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/DeleteCustomerRecord/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { QueryDeleteCustomerRecordParams } from '@/pages/StaffAdmin/DeleteCustomerRecord/data'; 4 | 5 | // 删人提醒 6 | export async function QueryDeleteCustomerRecord(params?: QueryDeleteCustomerRecordParams) { 7 | return request(`${StaffAdminApiPrefix}/notify/delete-customers`, { 8 | params, 9 | }); 10 | } 11 | 12 | // 导出删人提醒 13 | export async function ExportDeleteCustomerRecord(params?: QueryDeleteCustomerRecordParams) { 14 | return request(`${StaffAdminApiPrefix}/staff/action/delete-customers-data-export`, { 15 | responseType: 'blob', 16 | params, 17 | }); 18 | } 19 | 20 | export interface DeleteCustomerRecordNotifyRuleInterface { 21 | is_notify_staff: number; 22 | notify_type: number; 23 | ext_staff_ids: string[]; 24 | } 25 | 26 | export async function UpdateDeleteCustomerRecordRule( 27 | params: DeleteCustomerRecordNotifyRuleInterface, 28 | ) { 29 | return request(`${StaffAdminApiPrefix}/notify/delete-customer/status`, { 30 | method: 'PUT', 31 | data: { 32 | ...params, 33 | }, 34 | }); 35 | } 36 | 37 | export async function GetDeleteCustomerRecordNotifyRule() { 38 | return request(`${StaffAdminApiPrefix}/notify/delete-customer/status`, { 39 | method: 'GET', 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChat/data.d.ts: -------------------------------------------------------------------------------- 1 | import type {CommonQueryParams} from '@/services/common'; 2 | 3 | export type GroupChatItem = Partial<{ 4 | id: string; 5 | ext_corp_id: string; 6 | ext_creator_id: string; 7 | ext_chat_id: string; 8 | name: string; 9 | owner: string; 10 | owner_name: string; 11 | create_time: Date; 12 | notice: string; 13 | member_list?: any; 14 | admin_list?: any; 15 | status: number; 16 | total: number; 17 | today_join_member_num: number; 18 | today_quit_member_num: number; 19 | tags: any[]; 20 | owner_avatar_url: string; 21 | owner_role_type: string; 22 | created_at: Date; 23 | updated_at: Date; 24 | deleted_at?: any; 25 | }>; 26 | 27 | export type QueryGroupChatParams = Partial<{ 28 | create_time_end: string; 29 | create_time_start: string; 30 | group_tag_ids: number[]; 31 | name: string; 32 | owners: string[]; 33 | status: number; 34 | tags_union_type: string; 35 | }> & CommonQueryParams; 36 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChat/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .tag { 4 | margin: 6px 3px 6px 0; 5 | padding: 2px 8px 2px 0; 6 | vertical-align: center; 7 | 8 | .icon { 9 | width: 32px !important; 10 | height: 32px !important; 11 | margin-right: 6px; 12 | font-size: 16px; 13 | border-radius: 4px; 14 | 15 | svg { 16 | width: 16px; 17 | height: 16px; 18 | } 19 | } 20 | 21 | .text { 22 | color: @text-color-secondary; 23 | font-size: 14px; 24 | vertical-align: -1px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChat/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 3 | import type { QueryGroupChatParams } from '@/pages/StaffAdmin/GroupChat/data'; 4 | import type { GroupChatItem } from '@/pages/StaffAdmin/GroupChat/data'; 5 | 6 | // 查询客户群记录 7 | export async function QueryCustomerGroupsList(params?: QueryGroupChatParams) { 8 | return request(`${StaffAdminApiPrefix}/group-chats`, { 9 | params, 10 | }); 11 | } 12 | 13 | // 导出记录 14 | export async function ExportCustomerGroupsList(params?: QueryGroupChatParams) { 15 | return request(`${StaffAdminApiPrefix}/group-chat/action/export`, { 16 | responseType: 'blob', 17 | params, 18 | }); 19 | } 20 | 21 | export async function QueryCustomerGroupsOwners(params?: GroupChatItem) { 22 | return request(`${StaffAdminApiPrefix}/group-chat/owners`, { 23 | params, 24 | }); 25 | } 26 | 27 | export type UpdateGroupChatTagsParams = { 28 | group_chat_ids: string[]; 29 | add_tag_ids: string[]; 30 | remove_tag_ids: string[]; 31 | }; 32 | 33 | 34 | // 批量打标签 35 | export async function UpdateGroupChatTags(params: UpdateGroupChatTagsParams) { 36 | return request(`${StaffAdminApiPrefix}/group-chat/action/update-tags`, { 37 | method: 'POST', 38 | data: { 39 | ...params, 40 | }, 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChatTag/data.d.ts: -------------------------------------------------------------------------------- 1 | import type {CommonQueryParams} from '@/services/common'; 2 | 3 | export type GroupChatTagGroupItem = Partial<{ 4 | id: string; 5 | ext_corp_id: string; 6 | ext_creator_id: string; 7 | customer_group_tag_id: string; 8 | name: string; 9 | tags: GroupChatTag[]; 10 | delete_tag_ids: string[]; 11 | }>; 12 | 13 | export type GroupChatTag = Partial<{ 14 | id: string; 15 | ext_corp_id: string; 16 | ext_creator_id: string; 17 | customer_group_tag_group_id: string; 18 | name: string; 19 | }>; 20 | 21 | export type QueryGroupChatTagGroupParams = { 22 | name?: string; 23 | } & CommonQueryParams; 24 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChatTag/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .tagGroupList { 4 | .tagGroupItem { 5 | padding: 32px 0; 6 | border-bottom: 1px dashed #e8e8e8; 7 | 8 | .tagName { 9 | margin-top: 12px; 10 | 11 | > h4 { 12 | display: flex; 13 | flex-wrap: wrap; 14 | color: #222; 15 | font-weight: 600 !important; 16 | word-break: break-all; 17 | } 18 | } 19 | 20 | .tagList { 21 | margin-top: 12px; 22 | 23 | @media (max-width: @screen-xs) { 24 | margin-top: 12px; 25 | } 26 | 27 | .tagItem { 28 | padding: 5px 16px; 29 | color: @text-color-secondary; 30 | font-size: 14px; 31 | } 32 | } 33 | 34 | .groupAction { 35 | display: flex; 36 | justify-content: flex-end; 37 | flex-wrap: wrap; 38 | margin-top: 12px; 39 | 40 | @media (max-width: @screen-xs) { 41 | margin-top: 12px; 42 | } 43 | 44 | > button { 45 | padding: 2px 6px; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/GroupChatTag/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../../../config/constant'; 3 | import type { 4 | GroupChatTagGroupItem, 5 | QueryGroupChatTagGroupParams, 6 | } from '@/pages/StaffAdmin/GroupChatTag/data'; 7 | 8 | // 查询 9 | export async function Query(params?: QueryGroupChatTagGroupParams) { 10 | return request(`${StaffAdminApiPrefix}/group-chat/tag-groups`, { 11 | method: 'GET', 12 | params, 13 | }); 14 | } 15 | 16 | // 删除 17 | export async function Delete(params: { ext_ids: string[] }) { 18 | return request(`${StaffAdminApiPrefix}/group-chat/tag-group/action/delete`, { 19 | method: 'POST', 20 | data: { 21 | ...params, 22 | }, 23 | }); 24 | } 25 | 26 | // 新建 27 | export async function Create(params: GroupChatTagGroupItem) { 28 | return request(`${StaffAdminApiPrefix}/group-chat/tag-group`, { 29 | method: 'POST', 30 | data: { 31 | ...params, 32 | }, 33 | }); 34 | } 35 | 36 | export type CreateGroupChatTagParams = { 37 | group_id: string; 38 | names: string[]; 39 | }; 40 | 41 | 42 | // 加标签 43 | export async function CreateTags(params: CreateGroupChatTagParams) { 44 | return request(`${StaffAdminApiPrefix}/group-chat/tag`, { 45 | method: 'POST', 46 | data: { 47 | ...params, 48 | }, 49 | }); 50 | } 51 | 52 | // 更新 53 | export async function Update( 54 | params: GroupChatTagGroupItem 55 | ) { 56 | return request(`${StaffAdminApiPrefix}/group-chat/tag-group`, { 57 | method: 'PUT', 58 | data: { 59 | ...params, 60 | }, 61 | }); 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Login/callback.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { history } from '@@/core/history'; 3 | import { connect } from 'umi'; 4 | import type { Dispatch } from '@@/plugin-dva/connect'; 5 | 6 | export type LoginCallbackProps = { 7 | dispatch: Dispatch; 8 | }; 9 | 10 | const LoginCallback: React.FC = (props) => { 11 | useEffect(() => { 12 | const { dispatch } = props; 13 | if (dispatch) { 14 | dispatch({ 15 | type: 'staffAdmin/getCurrent', 16 | }); 17 | } 18 | }, [props]); 19 | return <>{history.push('/staff-admin/welcome')}; 20 | }; 21 | 22 | export default connect(() => ({}))(LoginCallback); 23 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Login/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme.less'; 2 | 3 | .main { 4 | width: 400px; 5 | margin: 0 auto; 6 | padding: 20px 36px 0; 7 | background-color: #fff; 8 | border-radius: 6px; 9 | box-shadow: 1px 1px 12px 1px rgba(33, 33, 33, 0.025); 10 | @media screen and (max-width: @screen-sm) { 11 | width: 95%; 12 | max-width: 328px; 13 | } 14 | 15 | .header { 16 | margin: 24px 0 0 0; 17 | text-align: center; 18 | 19 | .title { 20 | color: #32507c; 21 | } 22 | 23 | .desc { 24 | margin-top: 0; 25 | color: @text-color-secondary; 26 | font-size: @font-size-base; 27 | } 28 | } 29 | 30 | .placeholder { 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | width: 328px; 35 | height: 400px; 36 | } 37 | 38 | .qrcodeContainer { 39 | display: flex; 40 | justify-content: center; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Login/qrcode.css: -------------------------------------------------------------------------------- 1 | .impowerBox .title { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/Article/index.less: -------------------------------------------------------------------------------- 1 | .header{ 2 | display: flex; 3 | flex-direction: column; 4 | .tabButtonContainer{ 5 | width: 100%; 6 | } 7 | .contentContainer{ 8 | width: 100%; 9 | .topNav{ 10 | width: 100%; 11 | margin:26px 0; 12 | display: flex; 13 | flex-direction: row; 14 | .topNavTitle{ 15 | width: 50%; 16 | font-size: 16px; 17 | color: rgba(0,0,0,.85); 18 | font-weight: 600; 19 | line-height: 22px; 20 | } 21 | .topNavOperator{ 22 | width: 50%; 23 | display: flex; 24 | justify-content: flex-end; 25 | } 26 | } 27 | .noneArticles{ 28 | width: 100%; 29 | padding: 60px 24px 0 0; 30 | display: flex; 31 | flex-direction:column; 32 | justify-content: center; 33 | align-items: center; 34 | p{ 35 | margin: 16px 0; 36 | color:rgba(0,0,0,0.45); 37 | } 38 | } 39 | .articles{ 40 | width: 100%; 41 | padding: 60px 24px 0 0; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/Article/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Button, Input} from 'antd' 3 | import empty from '@/assets/empty.png' 4 | import styles from './index.less' 5 | 6 | const Article: React.FC = () => { 7 | const [currentArticalType,setCurrentArticalType] = useState(1) 8 | const [wxArticles] = useState([]) 9 | const [scrmArticles] = useState([]) 10 | 11 | return ( 12 |
13 |
14 |
15 | 19 | 23 |
24 |
25 | { 26 | currentArticalType===1 &&
27 |
28 |
29 | 文章素材(共{}篇) 30 |
31 |
32 | 33 | 34 |
35 |
36 | { 37 | scrmArticles.length===0 ?
38 | 39 |

暂无文章素材

40 | 41 |
42 | : 43 |
44 |
45 | } 46 |
47 | } 48 | { 49 | currentArticalType===0 &&
50 | { 51 | wxArticles.length===0 ?
52 | 53 |

暂无文章素材

54 |

可添加公众号,同步公众号文章素材

55 | 56 |
57 | : 58 |
59 |
60 | } 61 |
62 | 63 | } 64 |
65 | 66 | 67 |
68 |
69 | ); 70 | } 71 | export default Article; 72 | 73 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/TagProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {QueryMaterialLibraryTags} from './service' 3 | import {message} from 'antd' 4 | import type {Dispatch, SetStateAction} from 'react'; 5 | import type {Dictionary} from 'lodash'; 6 | import _ from 'lodash' 7 | 8 | type TagContextParams = { 9 | allTags: MaterialTag.Item[]; 10 | setAllTags: Dispatch>; 11 | setTagsItemsTimestamp: Dispatch>; 12 | allTagsMap: Dictionary; 13 | } 14 | export const TagContext = React.createContext({} as TagContextParams) 15 | 16 | const TagProvider: React.FC = (props) => { 17 | const [allTags, setAllTags] = React.useState([]) 18 | const [tagsTimestamp, setTagsItemsTimestamp] = React.useState(Date.now); 19 | const [allTagsMap, setAllTagsMap] = React.useState>({}) 20 | const getTagList = (name?: string) => { 21 | QueryMaterialLibraryTags({page_size: 5000, name}).then((res) => { 22 | if (res?.code === 0) { 23 | setAllTags(res?.data?.items) 24 | setAllTagsMap(_.keyBy(res?.data?.items, 'id')) 25 | } else { 26 | message.error(res?.message); 27 | } 28 | }) 29 | } 30 | React.useEffect(() => { 31 | getTagList() 32 | }, [tagsTimestamp]) 33 | return ( 34 | 35 | {props.children} 36 | 37 | ) 38 | } 39 | 40 | export default TagProvider; 41 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/components/TagModal/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../../theme.less'; 2 | @import '../../../../../styles/component'; 3 | 4 | .tagList { 5 | margin: 15px 0 30px 0; 6 | @media (max-width: @screen-xs) { 7 | margin-top: 6px; 8 | } 9 | 10 | :global { 11 | .tag-item { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | min-width: 40px; 16 | height: 32px; 17 | //margin: 1px; 18 | text-align: center; 19 | //color: #6b7a88; 20 | color: @text-color-secondary; 21 | padding: 4px 8px; 22 | font-size: 14px; 23 | text-align: center; 24 | //margin-bottom: 8px; 25 | background-color: rgb(241, 250, 255); 26 | border: 1px solid rgb(241, 250, 255); 27 | } 28 | 29 | .selected-tag-item { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | min-width: 40px; 34 | height: 32px; 35 | margin: 1px; 36 | color: @primary-color; 37 | font-size: 14px; 38 | text-align: center; 39 | background-color: rgb(231, 247, 255); 40 | } 41 | } 42 | } 43 | 44 | .groupAction { 45 | display: flex; 46 | flex-wrap: wrap; 47 | margin-top: 12px; 48 | 49 | @media (max-width: @screen-xs) { 50 | margin-top: 12px; 51 | } 52 | 53 | > button { 54 | padding: 2px 6px; 55 | } 56 | } 57 | 58 | .tagGroupItem:last-child { 59 | border-bottom: none; 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/data.d.ts: -------------------------------------------------------------------------------- 1 | declare module MaterialTag { 2 | export interface Item { 3 | id: string; 4 | ext_corp_id: string; 5 | ext_creator_id: string; 6 | name: string; 7 | created_at: Date; 8 | updated_at: Date; 9 | deleted_at?: any; 10 | } 11 | } 12 | 13 | declare module Material { 14 | export interface Item { 15 | id: string; 16 | ext_corp_id: string; 17 | ext_creator_id: string; 18 | material_type: string; 19 | title: string; 20 | content: string; 21 | digest: string; 22 | file_size: string; 23 | media_id: string; 24 | publish_status: string; 25 | show_cover: number; 26 | thumb_media_id: string; 27 | thumb_url: string; 28 | url: string; 29 | link: string; 30 | material_tag_list?: any; 31 | created_at: Date; 32 | updated_at: Date; 33 | deleted_at?: any; 34 | } 35 | } 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/MaterialLibrary/index.less: -------------------------------------------------------------------------------- 1 | .banner { 2 | width: 100%; 3 | height: 50px; 4 | line-height: 50px; 5 | background-color: #8b9fbb; 6 | } 7 | .header { 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: space-between; 11 | width: 100%; 12 | min-height: 36px; 13 | margin-bottom: 16px; 14 | line-height: 36px; 15 | -webkit-box-pack: justify; 16 | } 17 | .deviderLine { 18 | width: 100%; 19 | height: 1px; 20 | margin-bottom: 16px; 21 | background: #e8e8e8; 22 | } 23 | .contentBox { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Role/Components/index.less: -------------------------------------------------------------------------------- 1 | .permissionList { 2 | padding: 0 0 0 12px; 3 | max-width: 600px; 4 | .routeTable { 5 | margin-bottom: 24px; 6 | :global{ 7 | .ant-table-thead{ 8 | .ant-table-cell{ 9 | color: #666666; 10 | font-weight: 600; 11 | } 12 | } 13 | 14 | td.ant-table-cell:first-child{ 15 | color: #666666; 16 | } 17 | } 18 | } 19 | 20 | .permissionItem { 21 | .title { 22 | font-size: 14px; 23 | font-weight: 600; 24 | color: #333; 25 | margin-bottom: 16px; 26 | 27 | .icon { 28 | margin-right: 6px; 29 | } 30 | 31 | .name { 32 | 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Role/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { message } from 'antd/es'; 4 | import { history } from 'umi'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type { CommonResp } from '@/services/common'; 8 | import { Create } from '@/pages/StaffAdmin/Role/service'; 9 | import RoleForm from '@/pages/StaffAdmin/Role/Components/form'; 10 | import type { RoleItem } from '@/pages/StaffAdmin/Role/data'; 11 | import type { FormInstance } from 'antd'; 12 | import { False } from '../../../../config/constant'; 13 | 14 | const CreateRole: React.FC = () => { 15 | const [currentRole] = useState({ 16 | is_default: False, 17 | }); 18 | 19 | const roleForm = useRef(); 20 | 21 | return ( 22 | history.goBack()} 24 | backIcon={} 25 | header={{ 26 | title: '创建角色', 27 | }} 28 | > 29 | 30 | { 35 | const params = { ...values }; 36 | const hide = message.loading('处理中'); 37 | const res: CommonResp = await Create(params); 38 | hide(); 39 | if (res.code === 0) { 40 | history.push('/staff-admin/company-management/role'); 41 | message.success('添加成功'); 42 | return true; 43 | } 44 | 45 | if (res.message) { 46 | message.error(res.message); 47 | return false; 48 | } 49 | 50 | message.error('添加失败'); 51 | return false; 52 | }} 53 | currentItem={currentRole} 54 | /> 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default CreateRole; 61 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Role/data.d.ts: -------------------------------------------------------------------------------- 1 | import type {CommonQueryParams} from '@/services/common'; 2 | 3 | export type RoleItem = Partial<{ 4 | id: string; 5 | ext_corp_id: string; 6 | ext_creator_id: string; 7 | name: string; 8 | description: string; 9 | type: string; 10 | count: number; 11 | sort_weight: number; 12 | is_default: number; 13 | permission_ids: string[]; 14 | created_at: Date; 15 | updated_at: Date; 16 | deleted_at?: any; 17 | }>; 18 | 19 | 20 | export type CreateRoleParam = { 21 | name: string; 22 | description: string; 23 | permission_ids: string[]; 24 | }; 25 | 26 | export type UpdateRoleParam = Partial<{ 27 | id: string; 28 | name: string; 29 | description: string; 30 | permission_ids: string[]; 31 | }>; 32 | 33 | // AssignRoleToStaffParam 对员工授予角色 34 | export type AssignRoleToStaffParam = { 35 | role_id: string; 36 | ext_staff_ids: string[]; 37 | }; 38 | 39 | // QueryRoleStaffsParam 查询某角色下的员工列表 40 | export type QueryRoleStaffsParam = Partial<{ 41 | staff_id: string; 42 | ext_staff_id: string; 43 | name: string; 44 | role_id: string; 45 | role_type: string; 46 | }> & CommonQueryParams; 47 | 48 | 49 | export type QueryRoleParam = { 50 | name?: string; 51 | } & CommonQueryParams; 52 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Role/edit.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { message } from 'antd/es'; 4 | import { history } from 'umi'; 5 | import { LeftOutlined } from '@ant-design/icons'; 6 | import ProCard from '@ant-design/pro-card'; 7 | import type { CommonResp } from '@/services/common'; 8 | import { Get, Update } from '@/pages/StaffAdmin/Role/service'; 9 | import RoleForm from '@/pages/StaffAdmin/Role/Components/form'; 10 | import type { RoleItem } from '@/pages/StaffAdmin/Role/data'; 11 | import type { FormInstance } from 'antd'; 12 | import { False } from '../../../../config/constant'; 13 | 14 | const CreateRole: React.FC = () => { 15 | const [currentRole, setCurrentRole] = useState({ 16 | is_default: False, 17 | }); 18 | const [itemID, setItemID] = useState(''); 19 | const roleFormRef = useRef(); 20 | 21 | useEffect(() => { 22 | const params = new URLSearchParams(window.location.search); 23 | const id = params.get('id'); 24 | if (id) { 25 | setItemID(id); 26 | } else { 27 | message.error('传入参数请带上ID'); 28 | } 29 | }, []); 30 | 31 | useEffect(() => { 32 | if (itemID) { 33 | const hide = message.loading('加载数据中'); 34 | Get(itemID).then((res) => { 35 | hide(); 36 | if (res.code === 0) { 37 | setCurrentRole(res.data); 38 | roleFormRef.current?.setFieldsValue(res.data); 39 | } else { 40 | message.error(res.message); 41 | } 42 | }); 43 | } 44 | }, [itemID]); 45 | 46 | return ( 47 | history.goBack()} 49 | backIcon={} 50 | header={{ 51 | title: '修改角色', 52 | }} 53 | > 54 | 55 | { 60 | const hide = message.loading('处理中'); 61 | const res: CommonResp = await Update(values); 62 | hide(); 63 | if (res.code === 0) { 64 | history.push('/staff-admin/company-management/role'); 65 | message.success('修改成功'); 66 | return true; 67 | } 68 | 69 | if (res.message) { 70 | message.error(res.message); 71 | return false; 72 | } 73 | 74 | message.error('修改失败'); 75 | return false; 76 | }} 77 | currentItem={currentRole} 78 | /> 79 | 80 | 81 | ); 82 | }; 83 | 84 | export default CreateRole; 85 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Role/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../../../config/constant'; 3 | import type { 4 | AssignRoleToStaffParam, 5 | CreateRoleParam, 6 | QueryRoleParam, 7 | QueryRoleStaffsParam, 8 | UpdateRoleParam 9 | } from "@/pages/StaffAdmin/Role/data"; 10 | 11 | export async function Query(params?: QueryRoleParam) { 12 | return request(`${StaffAdminApiPrefix}/roles`, { 13 | params: { 14 | ...params, 15 | sort_type: 'desc', 16 | sort_field: 'sort_weight', 17 | }, 18 | }); 19 | } 20 | 21 | export async function Create(params: CreateRoleParam) { 22 | return request(`${StaffAdminApiPrefix}/role`, { 23 | method: 'POST', 24 | data: { 25 | ...params, 26 | }, 27 | }); 28 | } 29 | 30 | // AssignRoleToStaff 对员工授予角色 31 | export async function AssignRoleToStaff(params: AssignRoleToStaffParam) { 32 | return request(`${StaffAdminApiPrefix}/role/action/assign-to-staffs`, { 33 | method: 'POST', 34 | data: { 35 | ...params, 36 | }, 37 | }); 38 | } 39 | 40 | // 查询授权员工 41 | export async function QueryRoleStaff(params?: QueryRoleStaffsParam) { 42 | return request(`${StaffAdminApiPrefix}/role/action/query-staffs`, { 43 | params, 44 | }); 45 | } 46 | 47 | 48 | export async function Update(params: UpdateRoleParam) { 49 | return request(`${StaffAdminApiPrefix}/role/${params.id}`, { 50 | method: 'PUT', 51 | data: { 52 | ...params, 53 | }, 54 | }); 55 | } 56 | 57 | export async function Get(id: string) { 58 | return request(`${StaffAdminApiPrefix}/role/${id}`, { 59 | method: 'GET', 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ScriptLibrary/IndividualScript.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const IndividualScript: React.FC = () => { 4 | return ( 5 |
6 |
7 | ) 8 | } 9 | export default IndividualScript 10 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ScriptLibrary/TeamScript.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TeamScript: React.FC = () => { 4 | return ( 5 |
6 |
7 | ) 8 | } 9 | export default TeamScript 10 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ScriptLibrary/components/ExpandableParagraph.tsx: -------------------------------------------------------------------------------- 1 | import Paragraph from 'antd/es/typography/Paragraph'; 2 | import React, {Fragment, useState} from 'react'; 3 | 4 | 5 | export type ExpandableParagraphProps = { 6 | rows: number; // 列数 7 | content: string; // 文字内容 8 | } 9 | // 展开折叠文字 10 | export const ExpandableParagraph: React.FC = (props) => { 11 | const {rows, content} = props; 12 | const [key, setKey] = useState(0); 13 | const [expanded, setExpanded] = useState(false); 14 | return ( 15 | 16 |
17 | { 21 | setExpanded(true) 22 | } 23 | }}> 24 | {content} 25 | 26 |
27 | {expanded && { 28 | e.preventDefault(); 29 | setKey(key + 1) 30 | setExpanded(false) 31 | }}>折叠} 32 |
33 | ); 34 | }; 35 | 36 | export default ExpandableParagraph; 37 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ScriptLibrary/components/index.less: -------------------------------------------------------------------------------- 1 | .imageOverview { 2 | display: flex; 3 | width: 100%; 4 | margin: 8px 0; 5 | 6 | .leftPart { 7 | display: flex; 8 | justify-content: flex-start; 9 | align-items: center; 10 | 11 | img { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | } 16 | 17 | .rightPart { 18 | width: 50%; 19 | margin-left: 12px; 20 | font-size: 12px; 21 | 22 | p:nth-child(1) { 23 | margin-top: 10px; 24 | margin-bottom: 5px; 25 | overflow: hidden; 26 | font-weight: 700; 27 | white-space: nowrap; 28 | text-overflow: ellipsis; 29 | } 30 | 31 | p:nth-child(2) { 32 | overflow: hidden; 33 | color: #aaa; 34 | white-space: nowrap; 35 | text-overflow: ellipsis; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/ScriptLibrary/index.tsx: -------------------------------------------------------------------------------- 1 | import {PlusOutlined} from '@ant-design/icons'; 2 | import {PageContainer} from '@ant-design/pro-layout'; 3 | import {Button} from 'antd'; 4 | import React, {useRef} from 'react'; 5 | import EnterpriseScript from './EnterpriseScript'; 6 | import styles from './index.less' 7 | 8 | const ScriptLibrary: React.FC = () => { 9 | const enterpriseScriptRef = useRef({}) 10 | return ( 11 |
12 | } 20 | style={{marginRight: 6}} 21 | onClick={() => { 22 | enterpriseScriptRef.current.createEnterpriseScript() 23 | }} 24 | > 25 | 新建话术 26 | 27 | ]} 28 | 29 | > 30 | 33 | 34 | 35 |
36 | ); 37 | }; 38 | export default ScriptLibrary 39 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Staff/StaffDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import { LeftOutlined } from '@ant-design/icons'; 2 | import { PageContainer } from '@ant-design/pro-layout'; 3 | import { history } from 'umi'; 4 | 5 | const MemberDetailInfo = (props: any) => { 6 | console.log(props.location.query) 7 | return ( 8 | history.goBack()} 10 | backIcon={} 11 | header={{ 12 | title: '成员详情', 13 | }} 14 | > 15 | 16 | ) 17 | } 18 | 19 | export default MemberDetailInfo 20 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Staff/data.d.ts: -------------------------------------------------------------------------------- 1 | declare module StaffList { 2 | export interface Item { 3 | id: string; 4 | ext_corp_id: string; 5 | ext_staff_id: string; 6 | role_id: string; 7 | type: string; 8 | name: string; 9 | address: string; 10 | alias: string; 11 | avatar_url: string; 12 | email: string; 13 | gender: number; 14 | status: number; 15 | mobile: string; 16 | qr_code_url: string; 17 | telephone: string; 18 | enable: number; 19 | signature: string; 20 | external_position: string; 21 | external_profile: string; 22 | extattr: string; 23 | external_user_count: number; 24 | dept_ids: number[]; 25 | departments: any[]; 26 | welcome_msg_id?: any; 27 | created_at: Date; 28 | updated_at: Date; 29 | deleted_at?: any; 30 | enable_msg_arch: number; 31 | } 32 | 33 | export interface Pager { 34 | page: number; 35 | page_size: number; 36 | total_rows: number; 37 | } 38 | 39 | export interface Data { 40 | items: Item[]; 41 | pager: Pager; 42 | } 43 | 44 | export interface RootObject { 45 | code: number; 46 | message: string; 47 | data: Data; 48 | } 49 | } 50 | 51 | declare module DepartmentList { 52 | export interface Item { 53 | label: any; 54 | label: any; 55 | id: string; 56 | ext_corp_id: string; 57 | ext_id: number; 58 | name: string; 59 | ext_parent_id: number; 60 | order: number; 61 | welcome_msg_id: string; 62 | sub_departments?: any; 63 | created_at: Date; 64 | updated_at: Date; 65 | deleted_at?: any; 66 | staff_num: any; 67 | } 68 | 69 | export interface Pager { 70 | page: number; 71 | page_size: number; 72 | total_rows: number; 73 | } 74 | 75 | export interface Data { 76 | items: Item[]; 77 | pager: Pager; 78 | } 79 | 80 | export interface RootObject { 81 | code: number; 82 | message: string; 83 | data: Data; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Staff/service.ts: -------------------------------------------------------------------------------- 1 | 2 | import request from '@/utils/request'; 3 | import { StaffAdminApiPrefix } from '../../../../config/constant'; 4 | 5 | type QueryStaffsParams = Partial<{ 6 | ext_department_ids: number; 7 | name: string; 8 | page: number; 9 | page_size: number; 10 | role_id: number | string; 11 | sort_field: string; 12 | sort_type: string; 13 | total_rows: number; 14 | type: string; 15 | }> 16 | type QueryDepartmentParams = Partial<{ 17 | ext_dept_ids: number[] 18 | ext_parent_id: number, 19 | page: number, 20 | page_size: number, 21 | sort_field: string, 22 | sort_type: string, 23 | total_rows: number 24 | }> 25 | 26 | // 同步企微员工数据 27 | export async function Sync() { 28 | return request(`${StaffAdminApiPrefix}/staff`, { 29 | method: 'POST', 30 | }); 31 | } 32 | // 获取企微员工数据 33 | export async function QueryStaffsList(params?: QueryStaffsParams) { 34 | return request(`${StaffAdminApiPrefix}/staffs`, { 35 | params, 36 | }); 37 | } 38 | // 获取部门列表 39 | export async function QueryDepartmentList(params?: QueryDepartmentParams) { 40 | return request(`${StaffAdminApiPrefix}/departments`, { 41 | params, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/StaffAdmin/Welcome/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../../../config/constant'; 3 | 4 | export interface GetTrendParams { 5 | statistic_type: 'total' | 'increase' | 'decrease' | 'net_increase'; 6 | start_time: string; 7 | end_time: string; 8 | ext_staff_ids: string[]; 9 | } 10 | 11 | export interface TrendItem { 12 | number: number; 13 | date: string; 14 | } 15 | 16 | // 获取客户数据趋势 17 | export async function GetTrend(params: GetTrendParams) { 18 | return request(`${StaffAdminApiPrefix}/action/get-trend`, { 19 | params, 20 | }); 21 | } 22 | 23 | 24 | export interface SummaryResult { 25 | corp_name: string; 26 | total_staffs_num: number; 27 | total_customers_num: number; 28 | today_customers_increase: number; 29 | today_customers_decrease: number; 30 | total_groups_num: number; 31 | today_groups_increase: number; 32 | today_groups_decrease: number; 33 | } 34 | 35 | // 获取统计概况 36 | export async function GetSummary() { 37 | return request(`${StaffAdminApiPrefix}/action/get-summary`); 38 | } 39 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-restricted-globals */ 3 | /* eslint-disable no-underscore-dangle */ 4 | /* globals workbox */ 5 | workbox.core.setCacheNameDetails({ 6 | prefix: 'antd-pro', 7 | suffix: 'v1', 8 | }); 9 | // Control all opened tabs ASAP 10 | workbox.clientsClaim(); 11 | 12 | /** 13 | * Use precaching list generated by workbox in build process. 14 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching 15 | */ 16 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []); 17 | 18 | /** 19 | * Register a navigation route. 20 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route 21 | */ 22 | workbox.routing.registerNavigationRoute('/index.html'); 23 | 24 | /** 25 | * Use runtime cache: 26 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute 27 | * 28 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc. 29 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies 30 | */ 31 | 32 | /** Handle API requests */ 33 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); 34 | 35 | /** Handle third party requests */ 36 | workbox.routing.registerRoute( 37 | /^https:\/\/gw\.alipayobjects\.com\//, 38 | workbox.strategies.networkFirst(), 39 | ); 40 | workbox.routing.registerRoute( 41 | /^https:\/\/cdnjs\.cloudflare\.com\//, 42 | workbox.strategies.networkFirst(), 43 | ); 44 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); 45 | 46 | /** Response to client after skipping waiting with MessageChannel */ 47 | addEventListener('message', (event) => { 48 | const replyPort = event.ports[0]; 49 | const message = event.data; 50 | if (replyPort && message && message.type === 'skip-waiting') { 51 | event.waitUntil( 52 | self.skipWaiting().then( 53 | () => { 54 | replyPort.postMessage({ 55 | error: null, 56 | }); 57 | }, 58 | (error) => { 59 | replyPort.postMessage({ 60 | error, 61 | }); 62 | }, 63 | ), 64 | ); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/services/common.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import {StaffAdminApiPrefix} from "../../config/constant"; 3 | 4 | export interface CommonResp { 5 | code: number; 6 | message: string; 7 | data?: any; 8 | } 9 | 10 | export type Pager = { 11 | total_rows?: number; 12 | page_size?: number; 13 | page?: number; 14 | }; 15 | 16 | export type Sorter = { 17 | sort_field?: string; 18 | sort_type?: string; 19 | }; 20 | 21 | export type CommonQueryParams = { 22 | sort_field?: 'id' | 'created_at' | 'updated_at' | 'sort_weight'|'order'; 23 | sort_type?: 'asc' | 'desc'; 24 | 25 | page_size?: number; 26 | page?: number; 27 | }; 28 | 29 | export interface ParseURLResult { 30 | title: string; 31 | desc: string; 32 | img_url: string; 33 | link_url: string; 34 | } 35 | 36 | // ParseURL 解析URL 37 | export async function ParseURL(url: string) { 38 | return request(`${StaffAdminApiPrefix}/common/action/parse-link`, { 39 | method: 'POST', 40 | data: {url} 41 | }); 42 | } 43 | 44 | export interface GetSignedURLResult { 45 | upload_url: string; 46 | download_url: string; 47 | } 48 | 49 | // GetSignedURL 获取云存储上传地址 50 | export async function GetSignedURL(filename: string) { 51 | return request(`${StaffAdminApiPrefix}/common/action/get-signed-url`, { 52 | method: 'POST', 53 | data: {file_name: filename}, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/services/customer_tag_group.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../config/constant'; 3 | import type { CommonQueryParams, CommonResp, Pager } from '@/services/common'; 4 | 5 | export interface CustomerTagGroupInterface { 6 | id: string; 7 | ext_corp_id: string; 8 | ext_creator_id: string; 9 | ext_id: string; 10 | name: string; 11 | create_time: number; 12 | order: number; 13 | department_list?: number[]; 14 | tags: Tag[]; 15 | created_at: Date; 16 | updated_at: Date; 17 | deleted_at?: any; 18 | } 19 | 20 | export interface Tag { 21 | id: string; 22 | ext_corp_id: string; 23 | ext_creator_id: string; 24 | ext_id: string; 25 | ext_group_id: string; 26 | name: string; 27 | group_name: string; 28 | create_time: number; 29 | order: number; 30 | type: number; 31 | created_at: Date; 32 | updated_at: Date; 33 | deleted_at?: any; 34 | } 35 | 36 | export type QueryCustomerTagGroupParams = { 37 | ext_department_id?: number; // 部门ID 38 | name?: string; 39 | } & CommonQueryParams; 40 | 41 | // QueryCustomerTagGroup 查询客户标签组列表 42 | export async function QueryCustomerTagGroups(params: QueryCustomerTagGroupParams): Promise< 43 | { 44 | data?: { 45 | items?: CustomerTagGroupInterface[]; 46 | pager: Pager; 47 | }; 48 | } & CommonResp 49 | > { 50 | return request(`${StaffAdminApiPrefix}/customer/tag-groups`, { 51 | params, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/services/department.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../config/constant'; 3 | import type { CommonQueryParams } from '@/services/common'; 4 | 5 | export interface DepartmentInterface { 6 | staff_num?: any; 7 | id: string; 8 | ext_corp_id: string; 9 | ext_id: number; 10 | name: string; 11 | ext_parent_id: number; 12 | order: number; 13 | welcome_msg_id: string; 14 | sub_departments?: DepartmentInterface[]; 15 | created_at: Date; 16 | updated_at: Date; 17 | deleted_at?: any; 18 | } 19 | 20 | // QueryDepartment 查询部门 21 | export async function QueryDepartment(params: CommonQueryParams & { ext_dept_ids?: number[]; ext_parent_id?: string;}) { 22 | return request(`${StaffAdminApiPrefix}/departments`, { 23 | params, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/services/group_chat.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import type { CommonQueryParams } from '@/services/common'; 3 | import { StaffAdminApiPrefix } from '../../config/constant'; 4 | 5 | export interface GroupChatMainInfoItem { 6 | ext_chat_id: string; 7 | name: string; 8 | owner_name: string; 9 | } 10 | 11 | export type QueryGroupChatParams = CommonQueryParams; 12 | 13 | // QueryGroupChat 查询员工列表 14 | export async function QueryGroupChatMainInfo(params: QueryGroupChatParams) { 15 | return request(`${StaffAdminApiPrefix}/group-chat/action/get-all`, { 16 | method: 'POST', 17 | params, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/services/staff.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { StaffAdminApiPrefix } from '../../config/constant'; 3 | import type { CommonQueryParams, CommonResp, Pager } from '@/services/common'; 4 | 5 | export interface StaffInterface { 6 | id: string; 7 | ext_corp_id: string; 8 | ext_staff_id: string; 9 | role_id: string; 10 | type: string; 11 | name: string; 12 | address: string; 13 | alias: string; 14 | avatar_url: string; 15 | email: string; 16 | gender: number; 17 | status: number; 18 | mobile: string; 19 | qr_code_url: string; 20 | telephone: string; 21 | is_enabled: boolean; 22 | signature: string; 23 | external_position: string; 24 | external_profile: string; 25 | extattr: string; 26 | external_user_count: number; 27 | corp_id: string; 28 | dept_ids: number[]; 29 | order?: any; 30 | is_leader_in_dept?: any; 31 | welcome_msg_id: string; 32 | } 33 | 34 | export type QueryStaffParams = { 35 | ext_department_id?: number; // 部门ID 36 | name?: string; 37 | role_id?: string; // 角色ID 38 | role_type?: string; // 角色类型 Admin DepartmentAdmin Staff 39 | } & CommonQueryParams; 40 | 41 | // QueryStaff 查询员工列表 42 | export async function QueryStaffs(params: QueryStaffParams): Promise< 43 | { 44 | data?: { 45 | items?: StaffInterface[]; 46 | pager: Pager; 47 | }; 48 | } & CommonResp 49 | > { 50 | return request(`${StaffAdminApiPrefix}/staffs`, { 51 | params, 52 | }); 53 | } 54 | 55 | export interface SimpleStaffInterface { 56 | id: string; 57 | ext_id: string; 58 | avatar_url: string; 59 | role_type: string; 60 | name: string; 61 | departments: { 62 | ext_id: number; 63 | name: string; 64 | ext_parent_id: number; 65 | }[]; 66 | } 67 | 68 | // QuerySimpleStaffs 查询员工概况 69 | export async function QuerySimpleStaffs(params: QueryStaffParams): Promise< 70 | { 71 | data?: { 72 | items?: SimpleStaffInterface[]; 73 | pager: Pager; 74 | }; 75 | } & CommonResp 76 | > { 77 | return request(`${StaffAdminApiPrefix}/staff/action/get-all`, { 78 | params, 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/services/staffAdmin.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import {StaffAdminApiPrefix} from '../../config/constant'; 3 | 4 | export interface StaffAdminRoleInterface { 5 | id: string; 6 | ext_corp_id: string; 7 | tenant_id: string; 8 | name: string; 9 | description: string; 10 | type: string; 11 | count: number; 12 | sort_weight: number; 13 | is_default: number; 14 | permission_ids: string[]; 15 | } 16 | 17 | export interface StaffAdminInterface { 18 | id: string; 19 | ext_corp_id: string; 20 | ext_staff_id: string; 21 | role_id: string; 22 | type: string; 23 | name: string; 24 | address: string; 25 | alias: string; 26 | avatar_url: string; 27 | email: string; 28 | gender: number; 29 | status: number; 30 | mobile: string; 31 | qr_code_url: string; 32 | telephone: string; 33 | is_enabled: boolean; 34 | signature: string; 35 | external_position: string; 36 | external_profile: string; 37 | extattr: string; 38 | external_user_count: number; 39 | corp_id: string; 40 | dept_ids: number[]; 41 | order?: any; 42 | is_leader_in_dept?: any; 43 | welcome_msg_id: string; 44 | role?: StaffAdminRoleInterface; 45 | } 46 | 47 | // GetCurrentStaffAdmin 获取当前登录员工 48 | export async function GetCurrentStaffAdmin() { 49 | return request(`${StaffAdminApiPrefix}/action/get-current-staff`, { 50 | method: 'GET', 51 | }); 52 | } 53 | 54 | export interface GetStaffAdminLoginQrcodeResp { 55 | app_id: string; 56 | agent_id: number; 57 | redirect_uri: string; 58 | state: string; 59 | location_url: string; 60 | } 61 | 62 | export async function GetStaffAdminLoginQrcode(extCorpID: string, sourceURL: string) { 63 | return request(`${StaffAdminApiPrefix}/action/login`, { 64 | method: 'POST', 65 | data: {ext_corp_id: extCorpID, source_url: sourceURL}, 66 | }); 67 | } 68 | 69 | export async function StaffAdminForceLogin(extCorpID: string = "", extStaffID: string = "") { 70 | return request(`${StaffAdminApiPrefix}/action/force-login`, { 71 | method: 'POST', 72 | data: {ext_corp_id: extCorpID, ext_staff_id: extStaffID}, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/component.less: -------------------------------------------------------------------------------- 1 | .avatarAndName { 2 | display: -webkit-box; 3 | display: flex; 4 | align-items: center; 5 | color: rgba(0, 0, 0, 0.85); 6 | font-size: 14px; 7 | -webkit-box-align: center; 8 | 9 | .avatar { 10 | width: 26px; 11 | height: 26px; 12 | margin-right: 10px; 13 | border-radius: 2px; 14 | } 15 | 16 | span { 17 | em { 18 | color: #1890ff; 19 | font-style: normal; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/theme.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | 3 | @primary-color: #1966ff; 4 | @success-color: #19be6b; 5 | @text-color: #12141a; 6 | @font-family: 'Helvetica Neue;Helvetica;PingFang SC;Hiragino Sans GB;Microsoft YaHei;"\\5FAE\\8F6F\\96C5\\9ED1";Arial;sans-serif;'; 7 | @font-size-base: 14px; 8 | @border-radius-base: 4px; 9 | @border-radius-sm: 4px; 10 | @text-color-secondary: #55585c; 11 | @secondary-color: rgba(66, 66, 66, 0.5); 12 | 13 | //@primary-color: #2d8cf0; // 全局主色 14 | //@link-color: #2d8cf0; // 链接色 15 | //@success-color: #52c41a; // 成功色 16 | //@warning-color: #faad14; // 警告色 17 | //@error-color: #f5222d; // 错误色 18 | //@font-size-base: 14px; // 主字号 19 | //@heading-color: rgba(0; 0; 0; 0.85); // 标题色 20 | //@text-color: rgba(0; 0; 0; 0.65); // 主文本色 21 | //@text-color-secondary: rgba(0; 0; 0; 0.45); // 次文本色 22 | //@disabled-color: rgba(0; 0; 0; 0.25); // 失效色 23 | //@border-radius-base: 2px; // 组件/浮层圆角 24 | //@border-color-base: #d9d9d9; // 边框色 25 | //@box-shadow-base: 0 3px 6px -4px rgba(0; 0; 0; 0.12); 0 6px 16px 0 rgba(0; 0; 0; 0.08); 26 | //0 9px 28px 8px rgba(0; 0; 0; 0.05); // 浮层阴影 27 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '*.scss'; 5 | declare module '*.sass'; 6 | declare module '*.svg'; 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.gif'; 11 | declare module '*.bmp'; 12 | declare module '*.tiff'; 13 | declare module 'omit.js'; 14 | declare module 'numeral'; 15 | declare module '@antv/data-set'; 16 | declare module 'mockjs'; 17 | declare module 'react-fittext'; 18 | declare module 'bizcharts-plugin-slider'; 19 | 20 | // google analytics interface 21 | type GAFieldsObject = { 22 | eventCategory: string; 23 | eventAction: string; 24 | eventLabel?: string; 25 | eventValue?: number; 26 | nonInteraction?: boolean; 27 | }; 28 | 29 | interface Window { 30 | ga: ( 31 | command: 'send', 32 | hitType: 'event' | 'pageview', 33 | fieldsObject: GAFieldsObject | string, 34 | ) => void; 35 | reloadAuthorized: () => void; 36 | routerBase: string; 37 | } 38 | 39 | declare let ga: () => void; 40 | 41 | // preview.pro.ant.design only do not use in your production ; 42 | // preview.pro.ant.design Dedicated environment variable, please do not use it in your project. 43 | declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined; 44 | 45 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; 46 | -------------------------------------------------------------------------------- /src/utils/Authorized.ts: -------------------------------------------------------------------------------- 1 | import RenderAuthorize from '@/components/Authorized'; 2 | import { getAuthority } from './authority'; 3 | /* eslint-disable eslint-comments/disable-enable-pair */ 4 | /* eslint-disable import/no-mutable-exports */ 5 | let Authorized = RenderAuthorize(getAuthority()); 6 | 7 | // Reload the rights component 8 | const reloadAuthorized = (): void => { 9 | Authorized = RenderAuthorize(getAuthority()); 10 | }; 11 | 12 | /** Hard code block need it。 */ 13 | window.reloadAuthorized = reloadAuthorized; 14 | 15 | export { reloadAuthorized }; 16 | export default Authorized; 17 | 18 | -------------------------------------------------------------------------------- /src/utils/authority.ts: -------------------------------------------------------------------------------- 1 | import {reloadAuthorized} from './Authorized'; 2 | import {StaffAdminAuthority} from '../../config/constant'; 3 | 4 | // use localStorage to store the authority info, which might be sent from server in actual project. 5 | export function getAuthority(str?: string): string | string[] { 6 | const authorityString = 7 | typeof str === 'undefined' && localStorage ? localStorage.getItem('authority') : str; 8 | // authorityString could be admin, "admin", ["admin"] 9 | let authority; 10 | try { 11 | if (authorityString) { 12 | authority = JSON.parse(authorityString); 13 | } 14 | } catch (e) { 15 | authority = authorityString; 16 | } 17 | if (typeof authority === 'string') { 18 | return [authority]; 19 | } 20 | // preview.pro.ant.design only do not use in your production. 21 | // preview.pro.ant.design Dedicated environment variable, please do not use it in your project. 22 | if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { 23 | return [StaffAdminAuthority]; 24 | } 25 | return authority; 26 | } 27 | 28 | export function setAuthority(authority: string | string[]): void { 29 | const proAuthority = typeof authority === 'string' ? [authority] : authority; 30 | localStorage.setItem('authority', JSON.stringify(proAuthority)); 31 | // auto reload 32 | reloadAuthorized(); 33 | } 34 | 35 | export function CanSee(authority: string | string[]): boolean { 36 | const authorityString = localStorage.getItem('authority'); 37 | if (!authorityString) { 38 | return false; 39 | } 40 | const requiredAuthority = typeof authority === 'string' ? [authority] : authority; 41 | try { 42 | const authorities = JSON.parse(authorityString) || []; 43 | let result = false; 44 | requiredAuthority.forEach((item) => { 45 | if (authorities.includes(item)){ 46 | result = true 47 | } 48 | }) 49 | return result; 50 | } catch (e) { 51 | console.log('canSee failed', 'e', e) 52 | return false; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | /** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */ 2 | import { extend } from 'umi-request'; 3 | import { notification } from 'antd'; 4 | import { message } from 'antd/es'; 5 | import { history } from '@@/core/history'; 6 | 7 | // http 状态码 8 | const statusMessage: Record = { 9 | 200: '服务器成功返回请求的数据。', 10 | 201: '新建或修改数据成功。', 11 | 202: '一个请求已经进入后台排队(异步任务)。', 12 | 204: '删除数据成功。', 13 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 14 | 401: '用户没有权限(令牌、用户名、密码错误)。', 15 | 403: '用户得到授权,但是访问是被禁止的。', 16 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 17 | 406: '请求的格式不可得。', 18 | 410: '请求的资源被永久删除,且不会再得到的。', 19 | 422: '当创建一个对象时,发生一个验证错误。', 20 | 500: '服务器发生错误,请检查服务器。', 21 | 502: '网关错误。', 22 | 503: '服务不可用,服务器暂时过载或维护。', 23 | 504: '网关超时。', 24 | }; 25 | 26 | // 业务状态码 27 | const codeMessage: Record = { 28 | 0: '成功', 29 | 500: '服务器内部错误', 30 | 404: '未知错误', 31 | 10000400: '非法请求', 32 | 10000887: '无效登录态', 33 | 20000004: '无权访问', 34 | }; 35 | 36 | // 业务状态码统一处理器 37 | const bizCodeHandler = async (response: Response): Promise => { 38 | // 非json响应直接返回,不判断业务状态码,也没有业务状态码 39 | if ( 40 | !response.headers.has('content-type') || 41 | !response.headers.get('content-type')?.includes('application/json') 42 | ) { 43 | return response; 44 | } 45 | 46 | const data = await response.clone().json(); 47 | if (data && data.code !== 0) { 48 | if (codeMessage[data.code]) { 49 | message.error(codeMessage[data.code]); 50 | } 51 | if (data && data.code === 10000887) { 52 | history.push(`/staff-admin/login`); 53 | } 54 | } 55 | 56 | return response; 57 | }; 58 | 59 | /** 60 | * @zh-CN 异常处理程序 61 | * @en-US Exception handler 62 | */ 63 | const errorHandler = (error: { response: Response }): Response => { 64 | const { response } = error; 65 | if (response && response.status) { 66 | const errorText = statusMessage[response.status] || response.statusText; 67 | const { status, url } = response; 68 | notification.error({ 69 | message: `请求失败 ${status}: ${url}`, 70 | description: errorText, 71 | }); 72 | } else if (!response) { 73 | notification.error({ 74 | description: '连接服务器失败', 75 | message: '网络异常', 76 | }); 77 | } 78 | return response; 79 | }; 80 | 81 | /** 82 | * @en-US Configure the default parameters for request 83 | * @zh-CN 配置request请求时的默认参数 84 | */ 85 | const request = extend({ 86 | errorHandler, // default error handling 87 | credentials: 'include', // Does the default request bring cookies 88 | }); 89 | 90 | request.interceptors.response.use(bizCodeHandler); 91 | 92 | export default request; 93 | -------------------------------------------------------------------------------- /src/utils/utils.less: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | .clearfix() { 4 | zoom: 1; 5 | &::before, 6 | &::after { 7 | display: table; 8 | content: ' '; 9 | } 10 | &::after { 11 | clear: both; 12 | height: 0; 13 | font-size: 0; 14 | visibility: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { isUrl } from './utils'; 2 | 3 | describe('isUrl tests', (): void => { 4 | it('should return false for invalid and corner case inputs', (): void => { 5 | expect(isUrl([] as any)).toBeFalsy(); 6 | expect(isUrl({} as any)).toBeFalsy(); 7 | expect(isUrl(false as any)).toBeFalsy(); 8 | expect(isUrl(true as any)).toBeFalsy(); 9 | expect(isUrl(NaN as any)).toBeFalsy(); 10 | expect(isUrl(null as any)).toBeFalsy(); 11 | expect(isUrl(undefined as any)).toBeFalsy(); 12 | expect(isUrl('')).toBeFalsy(); 13 | }); 14 | 15 | it('should return false for invalid URLs', (): void => { 16 | expect(isUrl('foo')).toBeFalsy(); 17 | expect(isUrl('bar')).toBeFalsy(); 18 | expect(isUrl('bar/test')).toBeFalsy(); 19 | expect(isUrl('http:/example.com/')).toBeFalsy(); 20 | expect(isUrl('ttp://example.com/')).toBeFalsy(); 21 | }); 22 | 23 | it('should return true for valid URLs', (): void => { 24 | expect(isUrl('http://example.com/')).toBeTruthy(); 25 | expect(isUrl('https://example.com/')).toBeTruthy(); 26 | expect(isUrl('http://example.com/test/123')).toBeTruthy(); 27 | expect(isUrl('https://example.com/test/123')).toBeTruthy(); 28 | expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy(); 29 | expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy(); 30 | expect(isUrl('http://www.example.com/')).toBeTruthy(); 31 | expect(isUrl('https://www.example.com/')).toBeTruthy(); 32 | expect(isUrl('http://www.example.com/test/123')).toBeTruthy(); 33 | expect(isUrl('https://www.example.com/test/123')).toBeTruthy(); 34 | expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy(); 35 | expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/PuppeteerEnvironment.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const NodeEnvironment = require('jest-environment-node'); 3 | const getBrowser = require('./getBrowser'); 4 | 5 | class PuppeteerEnvironment extends NodeEnvironment { 6 | // Jest is not available here, so we have to reverse engineer 7 | // the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823 8 | setTimeout(timeout) { 9 | if (this.global.jasmine) { 10 | // eslint-disable-next-line no-underscore-dangle 11 | this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; 12 | } else { 13 | this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout; 14 | } 15 | } 16 | 17 | async setup() { 18 | const browser = await getBrowser(); 19 | const page = await browser.newPage(); 20 | this.global.browser = browser; 21 | this.global.page = page; 22 | } 23 | 24 | async teardown() { 25 | const { page, browser } = this.global; 26 | 27 | if (page) { 28 | await page.close(); 29 | } 30 | 31 | if (browser) { 32 | await browser.disconnect(); 33 | } 34 | 35 | if (browser) { 36 | await browser.close(); 37 | } 38 | } 39 | } 40 | 41 | module.exports = PuppeteerEnvironment; 42 | -------------------------------------------------------------------------------- /tests/beforeTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const { execSync } = require('child_process'); 4 | const { join } = require('path'); 5 | const findChrome = require('carlo/lib/find_chrome'); 6 | const detectInstaller = require('detect-installer'); 7 | 8 | const installPuppeteer = () => { 9 | // find can use package manger 10 | const packages = detectInstaller(join(__dirname, '../../')); 11 | // get installed package manger 12 | const packageName = packages.find(detectInstaller.hasPackageCommand) || 'npm'; 13 | console.log(`🤖 will use ${packageName} install puppeteer`); 14 | const command = `${packageName} ${packageName.includes('yarn') ? 'add' : 'i'} puppeteer`; 15 | execSync(command, { 16 | stdio: 'inherit', 17 | }); 18 | }; 19 | 20 | const initPuppeteer = async () => { 21 | try { 22 | // eslint-disable-next-line import/no-unresolved 23 | const findChromePath = await findChrome({}); 24 | const { executablePath } = findChromePath; 25 | console.log(`🧲 find you browser in ${executablePath}`); 26 | return; 27 | } catch (error) { 28 | console.log('🧲 no find chrome'); 29 | } 30 | 31 | try { 32 | require.resolve('puppeteer'); 33 | } catch (error) { 34 | // need install puppeteer 35 | await installPuppeteer(); 36 | } 37 | }; 38 | 39 | initPuppeteer(); 40 | -------------------------------------------------------------------------------- /tests/getBrowser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | const findChrome = require('carlo/lib/find_chrome'); 4 | 5 | const getBrowser = async () => { 6 | try { 7 | // eslint-disable-next-line import/no-unresolved 8 | const puppeteer = require('puppeteer'); 9 | const browser = await puppeteer.launch({ 10 | args: [ 11 | '--disable-gpu', 12 | '--disable-dev-shm-usage', 13 | '--no-first-run', 14 | '--no-zygote', 15 | '--no-sandbox', 16 | ], 17 | }); 18 | return browser; 19 | } catch (error) { 20 | // console.log(error) 21 | } 22 | 23 | try { 24 | // eslint-disable-next-line import/no-unresolved 25 | const puppeteer = require('puppeteer-core'); 26 | const findChromePath = await findChrome({}); 27 | const { executablePath } = findChromePath; 28 | const browser = await puppeteer.launch({ 29 | executablePath, 30 | args: [ 31 | '--disable-gpu', 32 | '--disable-dev-shm-usage', 33 | '--no-first-run', 34 | '--no-zygote', 35 | '--no-sandbox', 36 | ], 37 | }); 38 | return browser; 39 | } catch (error) { 40 | console.log('🧲 no find chrome'); 41 | } 42 | throw new Error('no find puppeteer'); 43 | }; 44 | 45 | module.exports = getBrowser; 46 | -------------------------------------------------------------------------------- /tests/run-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | /* eslint-disable eslint-comments/no-unlimited-disable */ 4 | const { spawn } = require('child_process'); 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | const { kill } = require('cross-port-killer'); 7 | 8 | const env = Object.create(process.env); 9 | env.BROWSER = 'none'; 10 | env.TEST = true; 11 | env.UMI_UI = 'none'; 12 | env.PROGRESS = 'none'; 13 | // flag to prevent multiple test 14 | let once = false; 15 | 16 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], { 17 | env, 18 | }); 19 | 20 | startServer.stderr.on('data', (data) => { 21 | // eslint-disable-next-line 22 | console.log(data.toString()); 23 | }); 24 | 25 | startServer.on('exit', () => { 26 | kill(process.env.PORT || 8000); 27 | }); 28 | 29 | console.log('Starting development server for e2e tests...'); 30 | startServer.stdout.on('data', (data) => { 31 | console.log(data.toString()); 32 | // hack code , wait umi 33 | if ( 34 | (!once && data.toString().indexOf('Compiled successfully') >= 0) || 35 | data.toString().indexOf('Theme generated successfully') >= 0 36 | ) { 37 | // eslint-disable-next-line 38 | once = true; 39 | console.log('Development server is started, ready to run tests.'); 40 | const testCmd = spawn( 41 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm', 42 | ['test', '--', '--maxWorkers=1', '--runInBand'], 43 | { 44 | stdio: 'inherit', 45 | }, 46 | ); 47 | testCmd.on('exit', (code) => { 48 | startServer.kill(); 49 | process.exit(code); 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react-jsx", 10 | "resolveJsonModule": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "allowJs": true, 18 | "skipLibCheck": true, 19 | "experimentalDecorators": true, 20 | "strict": true, 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@@/*": ["./src/.umi/*"] 24 | } 25 | }, 26 | "include": [ 27 | "mock/**/*", 28 | "src/**/*", 29 | "tests/**/*", 30 | "test/**/*", 31 | "__test__/**/*", 32 | "typings/**/*", 33 | "config/**/*", 34 | ".eslintrc.js", 35 | ".stylelintrc.js", 36 | ".prettierrc.js", 37 | "jest.config.js", 38 | "mock/*" 39 | ], 40 | "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"] 41 | } 42 | --------------------------------------------------------------------------------