diff --git a/README.md b/README.md index 34fd7e8..b09aa7f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ # Global sales -聊天式销售平台 + +销售平台 2.0 ## 开发设置 +所有命令都在 cmd 目录, + 1. 安装组件:npm install -2. 运行开发环境:npm run dev 或者 start.bat -3. 打包代码:npm run build 或者 build.bat +2. 运行开发环境:dev.bat +3. 打包代码:build.bat ## 版本设置 + npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git] npm version premajor --no-git-tag-version @@ -22,5 +26,10 @@ npm version patch --no-git-tag-version [聊天式销售平台需求文档](https://www.kdocs.cn/l/calaUjgmCmDA?from=docs&reqtype=kdocs&startTime=1703645330177&createDirect=true&newFile=true) ## vonage语音视频 + 安装模块 npm i @vonage/client-sdk +## 本机测试账号 + +GLOBAL_SALES_LOGIN_USER +{"userId":"383","userIdStr":"383,609","emailList":[{"opi_sn":383,"mat_sn":760,"email":"lyj@asiahighlights.com","default":false,"backup":false},{"opi_sn":383,"mat_sn":759,"email":"lyj@chinahighlights.com","default":false,"backup":true},{"opi_sn":383,"mat_sn":758,"email":"lyj@hainatravel.com","default":true,"backup":false}],"username":"廖一军","avatarUrl":"https://static-legacy.dingtalk.com/media/lALPBDDrhXr716HNAoDNAoA_640_640.png","mobile":"+86-18777396951","email":"lyj@hainatravel.com","whatsAppBusiness":"8617458471254","openId":"iioljiPmZ4RPoOYpkFiSn7IKAiEiE","accountList":[{"OPI_SN":383,"OPI_Code":"LYJ","OPI_NameCN":"廖一军","OPI_DEI_SN":7,"OPI_NameEN":"Jimmy Liow"},{"OPI_SN":609,"OPI_Code":"LYJAH","OPI_NameCN":"廖一军(ah)","OPI_DEI_SN":28,"OPI_NameEN":"Jimmy Liow"}]} diff --git a/src/components/LexicalEditor/Index.jsx b/src/components/LexicalEditor/Index.jsx index 5ec8e92..26bc9c9 100644 --- a/src/components/LexicalEditor/Index.jsx +++ b/src/components/LexicalEditor/Index.jsx @@ -43,6 +43,7 @@ import {useLexicalEditable} from '@lexical/react/useLexicalEditable'; import { $getRoot, $getSelection, $createParagraphNode } from 'lexical'; import { $generateHtmlFromNodes, $generateNodesFromDOM, } from '@lexical/html'; // import { } from '@lexical/clipboard'; +import { isEmpty } from '@/utils/commons'; import './styles.css'; @@ -87,7 +88,6 @@ function LexicalDefaultValuePlugin({ value = "" }= {}) { if (clear) { root.clear(); } - // console.log(nodes); const p = $createParagraphNode(); const _p = nodes.filter(n => n).forEach((n) => { @@ -100,41 +100,56 @@ function LexicalDefaultValuePlugin({ value = "" }= {}) { // root.append(...nodes.filter(n => n)); }; + // 默认值设置只用初始化一次; + // 空值不更新 HTML; useEffect(() => { - if (editor && value) { + if (editor && !isEmpty(value)) { editor.update(() => { updateHTML(editor, value, true); }); } - }, [value]); + }, []); return null; } -function MyOnChangePlugin({ onChange }) { +function MyOnChangePlugin({ ignoreHistoryMergeTagChange = true, ignoreSelectionChange = true, onChange }) { const [editor] = useLexicalComposerContext(); useEffect(() => { - return editor.registerUpdateListener(({ editorState }) => { - // const editorStateJSON = editorState.toJSON(); - let html; - let textContent; - editorState.read(() => { - const root = $getRoot(); - const textContent = root.getTextContent(); - // console.log('textContent', textContent); - - const html = $generateHtmlFromNodes(editor); - // console.log('html', html); - - // setEditorContent(content); - if (typeof onChange === 'function') { - onChange({ editorState, html, textContent }); + if (onChange) { + return editor.registerUpdateListener(({editorState, dirtyElements, dirtyLeaves, prevEditorState, tags}) => { + + if ( + (ignoreSelectionChange && + dirtyElements.size === 0 && + dirtyLeaves.size === 0) || + (ignoreHistoryMergeTagChange && tags.has('history-merge')) || + prevEditorState.isEmpty() + ) { + return; } + // const editorStateJSON = editorState.toJSON(); + let html; + let textContent; + editorState.read(() => { + const root = $getRoot(); + const textContent = root.getTextContent(); + // console.log('textContent', textContent); + + const html = $generateHtmlFromNodes(editor); + // console.log('html', html); + + // setEditorContent(content); + if (typeof onChange === 'function') { + onChange({ editorState, editor, tags, html, textContent }); + } + }); }); - }); - }, [editor, onChange]); + } + }, [editor, ignoreHistoryMergeTagChange, ignoreSelectionChange, onChange]); + return null; } -export default function Editor({ isRichText, editorRef, onChange, initialValue, ...props }) { +export default function Editor({ isRichText, isDebug, editorRef, onChange, initialValue, ...props }) { return (
@@ -147,7 +162,7 @@ export default function Editor({ isRichText, editorRef, onChange, initialValue, } ErrorBoundary={LexicalErrorBoundary} /> )} - {import.meta.env.DEV && } + {(import.meta.env.DEV && isDebug) && } diff --git a/src/components/LexicalEditor/styles.css b/src/components/LexicalEditor/styles.css index b06cd5b..48fb8e2 100644 --- a/src/components/LexicalEditor/styles.css +++ b/src/components/LexicalEditor/styles.css @@ -568,7 +568,7 @@ i.chevron-down { } .dropdown { - z-index: 5; + z-index: 1201; /* Ant Modal z-index: 1000, Drawer z-index: 1200, 大于它才能使用在 Modal */ display: block; position: absolute; box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1), diff --git a/src/main.jsx b/src/main.jsx index d57df98..d9dee63 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -24,6 +24,7 @@ import Unassign from '@/views/ChatUnassign' import ChatAssign from '@/views/Conversations/ChatAssign' import DingdingLogin from '@/views/dingding/Login' +import DingdingQRCode from '@/views/dingding/QRCode' import useAuthStore from '@/stores/AuthStore' import '@/assets/index.css' @@ -83,10 +84,12 @@ const router = createBrowserRouter([ { path: '/p', element: , + errorElement: , children: [ { path: 'dingding/login', element: }, { path: 'dingding/logout', element: }, { path: 'dingding/callback', element: }, + { path: 'dingding/qr-code', element: }, ], }, ]) diff --git a/src/stores/AuthStore.js b/src/stores/AuthStore.js index 9888c29..c93f20a 100644 --- a/src/stores/AuthStore.js +++ b/src/stores/AuthStore.js @@ -49,7 +49,8 @@ const useAuthStore = create((set, get) => ({ setLoginStatus(200) const json = await fetchJSON( - `https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/WhatsAppAuth`, + 'http://202.103.68.157:889/dingtalk/dingtalkwork/WhatsAppAuth', + //`https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/WhatsAppAuth`, { authCode }, ) @@ -62,6 +63,16 @@ const useAuthStore = create((set, get) => ({ return acc.OPI_SN }) .join(','), + emailList: json.result?.emaillist.map(item => { + return { + opi_sn: item.opi_sn, + mat_sn: item.mat_sn, + email: item.email, + default: item.Isdefaultemail == 1, + backup: item.Isbakemail == 1, + } + }), + // whatsAppBusiness: json.result.opicode, accountName: json.result.opicode, username: json.result.nick, avatarUrl: json.result.avatarUrl, diff --git a/src/stores/SnippetStore.js b/src/stores/SnippetStore.js new file mode 100644 index 0000000..64b22a6 --- /dev/null +++ b/src/stores/SnippetStore.js @@ -0,0 +1,36 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' +import { fetchJSON, postForm } from '@/utils/request' +import { API_HOST } from '@/config' +import { isNotEmpty, prepareUrl } from '@/utils/commons' + +const useSnippetStore = create(devtools((set, get) => ({ + + ownerList: [], + typeList: [], + + fetchParamList: async () => { + let fetchOwnerUrl = `${API_HOST}/v2/GetAutoDocParameters` + const params = {}; + + return fetchJSON(fetchOwnerUrl, params) + .then(json => { + if (json.errcode === 0) { + console.info(json) + set(() => ({ + ownerList: json?.result?.owner.map(item => { + return { value: item.vsn, label: item.vname } + }), + typeList: json?.result?.type.map(item => { + return { value: item.vsn, label: item.vname } + }) + })) + } else { + throw new Error(json?.errmsg + ': ' + json.errcode) + } + }) + + }, +}), { name: 'snippetStore' })) + +export default useSnippetStore \ No newline at end of file diff --git a/src/views/DesktopApp.jsx b/src/views/DesktopApp.jsx index dfb8203..9bb5774 100644 --- a/src/views/DesktopApp.jsx +++ b/src/views/DesktopApp.jsx @@ -13,6 +13,7 @@ import { Typography, theme, Badge, + Drawer, } from 'antd' import 'dayjs/locale/zh-cn' import { useEffect, useState } from 'react' @@ -24,6 +25,8 @@ import 'react-chat-elements/dist/main.css' import ReloadPrompt from './ReloadPrompt' import ClearCache from './ClearCache' +import SnippetList from './accounts/SnippetList' + import { BUILD_VERSION, BUILD_DATE } from '@/config' const { Header, Footer, Content } = Layout @@ -37,6 +40,14 @@ function DesktopApp() { const totalNotify = useConversationStore((state) => state.totalNotify) + const [drawerOpen, setDrawerOpen] = useState(false) + + const onClick = ({ key }) => { + if (key === 'snippet-list') { + setDrawerOpen(true) + } + } + let defaultPath = '/order/follow' if (href !== '/') { @@ -78,35 +89,42 @@ function DesktopApp() { return (
- - - - App logo + }}> + setDrawerOpen(false)} + open={drawerOpen}> + + + + + + App logo 销售平台 订单跟踪, + label: 订单跟踪, }, { key: '/order/chat', label: ( - + 在线聊天 0 ? totalNotify : undefined} @@ -119,34 +137,33 @@ function DesktopApp() { }, { key: '/callcenter/call', - label: 语音通话, + label: 语音通话, }, { key: '/chat/history', - label: 聊天记录, + label: 聊天记录, }, ]} /> + }}> 个人资料, + label: 个人资料, key: 'profile', }, { - label: 图文集管理, + label: '图文集管理', key: 'snippet-list', }, { type: 'divider' }, @@ -155,17 +172,16 @@ function DesktopApp() { { label: , key: 'clearcache' }, { type: 'divider' }, { - label: 退出, + label: 退出, key: 'logout', }, ], + onClick, }} - trigger={['click']} - > + trigger={['click']}> e.preventDefault()} - style={{ color: colorPrimary }} - > + style={{ color: colorPrimary }}> {loginUser?.username?.substring(1)} @@ -185,8 +201,7 @@ function DesktopApp() { margin: 0, minHeight: 280, background: colorBgContainer, - }} - > + }}> diff --git a/src/views/accounts/LexicalEditorInput.jsx b/src/views/accounts/LexicalEditorInput.jsx new file mode 100644 index 0000000..1eb7d26 --- /dev/null +++ b/src/views/accounts/LexicalEditorInput.jsx @@ -0,0 +1,27 @@ +import LexicalEditor from '@/components/LexicalEditor' + +const LexicalEditorInput = (props) => { + const { id, value = {}, onChange } = props + + const triggerChange = (changedValue) => { + onChange?.({ + ...value, + ...changedValue, + }) + } + + return ( + { + triggerChange({ + html: val.html, + }) + }} + initialValue={value.html} + /> + ) +} + +export default LexicalEditorInput diff --git a/src/views/accounts/Profile.jsx b/src/views/accounts/Profile.jsx index f1dae12..7c2a116 100644 --- a/src/views/accounts/Profile.jsx +++ b/src/views/accounts/Profile.jsx @@ -8,27 +8,80 @@ import { Tag, Divider, List, - Result, + Alert, Button, - Image, + Flex, Select, + Spin, + Form, + Typography, + QRCode, } from 'antd' -import { UserOutlined } from '@ant-design/icons' +import { + UserOutlined, + InfoCircleOutlined, + CloseCircleFilled, + ReloadOutlined, + CheckCircleFilled, +} from '@ant-design/icons' import useAuthStore from '@/stores/AuthStore' function Profile() { const loginUser = useAuthStore((state) => state.loginUser) useEffect(() => { + console.info(loginUser) // 测试错误捕获: // throw new Error('💥 CABOOM 💥') + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const customStatusRender = (info) => { + switch (info.status) { + case 'expired': + return ( +
+ {' '} + {info.locale?.expired} +

+ +

+
+ ) + case 'loading': + return ( + + +

Loading...

+
+ ) + case 'scanned': + return ( +
+ {' '} + {info.locale?.scanned} +
+ ) + default: + return null + } + } + return ( <> - - + + @@ -49,84 +102,101 @@ function Profile() { ) })} - - {loginUser.mobile} - - - + + + + + + + 邮箱
} + dataSource={loginUser.emailList} + renderItem={(item) => { + const isDefault = item.default ? ( + 默认 + ) : null + const isBackup = item.backup ? 备用 : null + return ( + + {item.email} {isDefault} + {isBackup} + + ) + }} + /> + - {/* {loginUser.email} */} - 邮箱} - dataSource={[ - loginUser.email, - 'christyluo@chinahighlights.com', - 'christyluo@chinahighlights.net', - 'christyluo@asiahighlights.com', - 'christyluo@asiahighlights.net', - 'christyluo@globalhighlights.com', - 'christyluo@globalhighlights.net', - 'christyluo@163.com', - ]} - renderItem={(item) => {item}} - /> + + 在系统上使用 WhatsApp + +
    +
  • 在手机上打开 WhatsApp
  • +
  • 点击“已关联的设备”,然后点击“关联新设备”
  • +
  • 将手机对准屏幕扫描二维码
  • +
+
+
- - } - title='登录成功' - extra={} - /> + + + + +
diff --git a/src/views/accounts/SnippetList.jsx b/src/views/accounts/SnippetList.jsx index f7d3232..7b4872d 100644 --- a/src/views/accounts/SnippetList.jsx +++ b/src/views/accounts/SnippetList.jsx @@ -1,94 +1,245 @@ -import { Conditional } from '@/components/Conditional' -import useAuthStore from '@/stores/AuthStore' -import useFormStore from '@/stores/FormStore' -import { useOrderStore } from '@/stores/OrderStore' -import { copy, isNotEmpty, isEmpty } from '@/utils/commons' -import { WhatsAppOutlined } from '@ant-design/icons' -import { Row, Col, Tag, List, Form, Input, Button, Space } from 'antd' -import dayjs from 'dayjs' -import { useCallback, useEffect, useState, useRef } from 'react' -import { Link } from 'react-router-dom' -import LexicalEditor from '@/components/LexicalEditor' -import {$generateNodesFromDOM} from '@lexical/html' +import { + Flex, + Row, + Col, + Tag, + List, + Form, + Input, + Button, + Space, + Modal, + Select +} from 'antd' +import { useState, useRef, useEffect } from 'react' +import LexicalEditorInput from './LexicalEditorInput' +import useSnippetStore from '@/stores/SnippetStore' function SnippetList() { const [form] = Form.useForm() + const [snippetForm] = Form.useForm() const editorRef = useRef(null) - const [editorContent, setEditorContent] = useState('initialContent') + + const [fetchParamList, ownerList, typeList] = + useSnippetStore(state => [state.fetchParamList, state.ownerList, state.typeList]) + + const [isSnippetModalOpen, setSnippetModalOpen] = useState(false) + const [editorContent, setEditorContent] = useState( + "

Discover the best of the world with one of the best-rated tour companies for personalized travel. With over 10,000+ reviews and a 98.8% 5-star rating, we focus on streamlining your planning and ensuring joyful travels. Whether it's a milestone celebration or a relaxing getaway, let us help create your beautiful journey.

", + ) + + const onSnippetFinish = (values) => { + console.log('onSnippetFinish:', values) + + // console.info(JSON.stringify(editorRef.current.getEditorState())) + } + + const onSnippetFailed = (error) => { + console.log('Failed:', error) + // form.resetFields() + } + + useEffect(() => { + fetchParamList() + }, []) return ( <> - -
- - + setSnippetModalOpen(false)} + destroyOnClose + forceRender + modalRender={(dom) => ( + + {dom} + + )}> + + - - + + - - + + - - + + - - - - - - - - - - ( - { - console.info(item) - setEditorContent('' + item + '') - }}> - - - 类型 - {item} - - -
王静
- -
-
- )} - /> - - -
-          

Discover China with the award-winning and best-rated tour company for personalized travel in China. Honored as China's Leading Tour Operator by the World Travel Awards, we boast 10,000+ reviews and a remarkable 98.8% 5-star rating. Our expertise in customizing personalized China explorations is backed by our company-managed local services across China. Explore and kickstart your personalized travel experience with just a click!

-
- {console.info('onChange')}} initialValue={editorContent} /> - -
+ +
+ + + + + + + + + + + + + + + +
+ + + ( + { + console.info(item) + setEditorContent('' + item + '') + }}> + + + 类型 + {item} + + +
王静
+ +
+
+ )} + /> + + +
+ +
+                  

+ Discover China with the award-winning and{' '} + best-rated tour company for{' '} + personalized travel in China. Honored as{' '} + China is Leading Tour Operator by the{' '} + World Travel Awards, we boast{' '} + 10,000+ reviews and a remarkable{' '} + 98.8% 5-star rating. Our expertise in + customizing personalized China explorations is backed by our + company-managed local services across China. Explore and + kickstart your personalized travel experience with just a + click! +

+
+ + + + + +
+
+ +
) diff --git a/src/views/dingding/QRCode.jsx b/src/views/dingding/QRCode.jsx index 6a6c77e..4b5e12a 100644 --- a/src/views/dingding/QRCode.jsx +++ b/src/views/dingding/QRCode.jsx @@ -1,6 +1,6 @@ import useAuthStore from '@/stores/AuthStore' -import { Flex, Result, Spin, Typography } from 'antd' -import { useEffect } from 'react' +import { Flex, Input, Button, Typography } from 'antd' +import { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' const { Title } = Typography @@ -8,22 +8,26 @@ const { Title } = Typography // 钉钉扫码开发文档:https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information#title-qpi-0qv-anm function QRCode() { - const navigate = useNavigate() - - const loginStatus = useAuthStore((state) => state.loginStatus) - const setLoginStatus = useAuthStore((state) => state.setLoginStatus) - const loginUser = useAuthStore((state) => state.loginUser) + + const [result, setResult] = useState('') + + // const loginStatus = useAuthStore((state) => state.loginStatus) + // const setLoginStatus = useAuthStore((state) => state.setLoginStatus) + // const loginUser = useAuthStore((state) => state.loginUser) const login = useAuthStore((state) => state.login) + const codeRef = useRef() useEffect(() => { - if (loginUser.userId > 0) { - navigate('/') - } - }, []) + // if (loginUser.userId > 0) { + // navigate('/') + // } + }, []) useEffect(() => { - import('https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js').then(() => { + import( + 'https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js' + ).then(() => { window.DTFrameLogin( { id: 'qrCodeContainer', @@ -31,7 +35,9 @@ function QRCode() { height: 300, }, { - redirect_uri: encodeURIComponent('https://sales.mycht.cn/p/dingding/callback'), + redirect_uri: encodeURIComponent( + 'https://sales.mycht.cn/p/dingding/callback', + ), client_id: 'dingwgdx6emlxr3fcrg8', scope: 'openid', response_type: 'code', @@ -40,49 +46,37 @@ function QRCode() { }, (loginResult) => { const { authCode } = loginResult - login(authCode) + setResult(authCode) }, (errorMsg) => { - setLoginStatus(403) + // setLoginStatus(403) console.error(`Login Error: ${errorMsg}`) }, ) }) }, []) - if (loginStatus === 200) { - return ( - - - ]} - /> - - ) - } else if (loginStatus === 302) { - navigate('/') - } else if (loginStatus === 403) { - return ( - - - - ) - } else { - return ( - - 使用钉钉扫码 -
-
- ) - } + return ( + + 使用钉钉扫码 +
+ 钉钉 authCode: {result} + + +
+ ) } -export default QRCode \ No newline at end of file +export default QRCode