├── .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 |
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 | 
54 | 
55 | 
56 | 
57 | 
58 | 
59 | 
60 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/file-icon-excel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/file-icon-image.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/file-icon-link.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/file-icon-ppt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/file-icon-video.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/file-icon-word.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/group-chat-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openscrm/dashboard/4649b3924c82e56cdd9d22227c7cf758a45cd4f3/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |

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 |
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 | }
23 | onClick={() => {
24 | setInputVisible(true);
25 | }}
26 | >
27 | 添加
28 |
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 |
--------------------------------------------------------------------------------