Merge remote-tracking branch 'origin/main'

2.0/email-builder
Lei OT 11 months ago
commit 5f7eb960bf

@ -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 [<newversion> | 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"}]}

@ -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,20 +100,33 @@ 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 }) => {
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;
@ -127,14 +140,16 @@ function MyOnChangePlugin({ onChange }) {
// setEditorContent(content);
if (typeof onChange === 'function') {
onChange({ editorState, html, textContent });
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 (
<LexicalComposer initialConfig={editorConfig}>
<div className='editor-container'>
@ -147,7 +162,7 @@ export default function Editor({ isRichText, editorRef, onChange, initialValue,
<PlainTextPlugin contentEditable={<ContentEditable className='editor-pure-input' />} ErrorBoundary={LexicalErrorBoundary} />
)}
<HistoryPlugin />
{import.meta.env.DEV && <TreeViewPlugin />}
{(import.meta.env.DEV && isDebug) && <TreeViewPlugin />}
<LexicalDefaultValuePlugin value={initialValue} />
<AutoFocusPlugin />
<CodeHighlightPlugin />

@ -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),

@ -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: <Standlone />,
errorElement: <ErrorPage />,
children: [
{ path: 'dingding/login', element: <DingdingLogin /> },
{ path: 'dingding/logout', element: <DingdingLogout /> },
{ path: 'dingding/callback', element: <DingdingCallback /> },
{ path: 'dingding/qr-code', element: <DingdingQRCode /> },
],
},
])

@ -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,

@ -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

@ -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 (
<Layout>
<Header
className="header"
className='header'
style={{
position: 'sticky',
top: 0,
zIndex: 2,
width: '100%',
background: 'white',
}}
>
<Row gutter={{ md: 24 }} align="middle">
<Col flex="220px">
<NavLink to="/">
<img src={AppLogo} className="logo" alt="App logo" />
}}>
<Drawer
title='图文集管理'
placement={'top'}
size={'large'}
onClose={() => setDrawerOpen(false)}
open={drawerOpen}>
<SnippetList></SnippetList>
</Drawer>
<Row gutter={{ md: 24 }} align='middle'>
<Col flex='220px'>
<NavLink to='/'>
<img src={AppLogo} className='logo' alt='App logo' />
</NavLink>
<Title level={3}>销售平台</Title>
</Col>
<Col span={10}>
<Menu
mode="horizontal"
mode='horizontal'
selectedKeys={[defaultPath]}
items={[
{
key: '/order/follow',
label: <Link to="/order/follow">订单跟踪</Link>,
label: <Link to='/order/follow'>订单跟踪</Link>,
},
{
key: '/order/chat',
label: (
<Link to="/order/chat">
<Link to='/order/chat'>
在线聊天
<Badge
count={totalNotify > 0 ? totalNotify : undefined}
@ -119,34 +137,33 @@ function DesktopApp() {
},
{
key: '/callcenter/call',
label: <Link to="/callcenter/call">语音通话</Link>,
label: <Link to='/callcenter/call'>语音通话</Link>,
},
{
key: '/chat/history',
label: <Link to="/chat/history">聊天记录</Link>,
label: <Link to='/chat/history'>聊天记录</Link>,
},
]}
/>
</Col>
<Col
flex="auto"
flex='auto'
style={{
color: 'white',
marginBottom: '0',
display: 'flex',
justifyContent: 'end',
}}
>
}}>
<ReloadPrompt />
<Dropdown
menu={{
items: [
{
label: <Link to="/account/profile">个人资料</Link>,
label: <Link to='/account/profile'>个人资料</Link>,
key: 'profile',
},
{
label: <Link to="/account/snippet-list">图文集管理</Link>,
label: '图文集管理',
key: 'snippet-list',
},
{ type: 'divider' },
@ -155,17 +172,16 @@ function DesktopApp() {
{ label: <ClearCache />, key: 'clearcache' },
{ type: 'divider' },
{
label: <Link to="/p/dingding/logout">退出</Link>,
label: <Link to='/p/dingding/logout'>退出</Link>,
key: 'logout',
},
],
onClick,
}}
trigger={['click']}
>
trigger={['click']}>
<a
onClick={(e) => e.preventDefault()}
style={{ color: colorPrimary }}
>
style={{ color: colorPrimary }}>
<Space>
<Avatar src={loginUser.avatarUrl}>
{loginUser?.username?.substring(1)}
@ -185,8 +201,7 @@ function DesktopApp() {
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}
>
}}>
<Outlet />
</Content>
</Layout>

@ -0,0 +1,27 @@
import LexicalEditor from '@/components/LexicalEditor'
const LexicalEditorInput = (props) => {
const { id, value = {}, onChange } = props
const triggerChange = (changedValue) => {
onChange?.({
...value,
...changedValue,
})
}
return (
<LexicalEditor
id={id}
{...{ isRichText: true }}
onChange={(val) => {
triggerChange({
html: val.html,
})
}}
initialValue={value.html}
/>
)
}
export default LexicalEditorInput

@ -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 (
<div>
<CloseCircleFilled
style={{
color: 'red',
}}
/>{' '}
{info.locale?.expired}
<p>
<Button type='link' onClick={info.onRefresh}>
<ReloadOutlined /> {info.locale?.refresh}
</Button>
</p>
</div>
)
case 'loading':
return (
<Space direction='vertical'>
<Spin />
<p>Loading...</p>
</Space>
)
case 'scanned':
return (
<div>
<CheckCircleFilled
style={{
color: 'green',
}}
/>{' '}
{info.locale?.scanned}
</div>
)
default:
return null
}
}
return (
<>
<Row>
<Col span={12} offset={6}>
<Descriptions title='个人资料' layout='vertical' column={2}>
<Col span={6} offset={6}>
<Descriptions title='个人资料' layout='vertical' column={1}>
<Descriptions.Item label='名字'>
<Space size='middle'>
<Avatar src={loginUser.avatarUrl}>
@ -49,28 +102,30 @@ function Profile() {
)
})}
</Descriptions.Item>
<Descriptions.Item label='手机'>
{loginUser.mobile}
</Descriptions.Item>
<Descriptions.Item label='商业号身份'>
<Space>
<Form
layout='vertical'
// form={form}
>
<Form.Item
tooltip={{
title: '你所属的业务部门',
icon: <InfoCircleOutlined />,
}}>
<Select
defaultValue='jack'
style={{
width: 200,
}}
defaultValue='8617607730395'
options={[
{
value: 'GH 顾问',
label: 'GH 顾问(18741256987)',
value: '8618174165365',
label: 'GH 客服(8618174165365)',
},
{
value: 'GH 客服',
label: 'GH 客服(13845214785)',
value: '8617607730395',
label: 'GH 顾问(8617607730395)',
},
{
value: 'GH 客运',
value: '191477856351',
label: 'GH 客运(191477856351)',
},
{
@ -90,43 +145,58 @@ function Profile() {
},
]}
/>
<Button type="primary">切换</Button>
</Space>
</Form.Item>
<Form.Item>
<Button type='primary'>保存</Button>
</Form.Item>
</Form>
</Descriptions.Item>
</Descriptions>
</Col>
<Col span={6}>
<List
header={<div>邮箱</div>}
dataSource={loginUser.emailList}
renderItem={(item) => {
const isDefault = item.default ? (
<Tag color='green'>默认</Tag>
) : null
const isBackup = item.backup ? <Tag color='cyan'>备用</Tag> : null
return (
<List.Item>
{item.email} {isDefault}
{isBackup}
</List.Item>
)
}}
/>
</Col>
</Row>
<Divider orientation='left'></Divider>
<Row>
<Col span={6} offset={6}>
{/* {loginUser.email} */}
<List
header={<div>邮箱</div>}
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) => <List.Item>{item}</List.Item>}
/>
<Typography>
<Typography.Title level={2}>在系统上使用 WhatsApp</Typography.Title>
<Typography.Paragraph>
<ul>
<li>在手机上打开 WhatsApp</li>
<li>点击已关联的设备然后点击关联新设备</li>
<li>将手机对准屏幕扫描二维码</li>
</ul>
</Typography.Paragraph>
</Typography>
</Col>
<Col span={6}>
<Result
icon={
<Image
preview={false}
width={200}
src='https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
/>
}
title='登录成功'
extra={<Button type='primary'>退出账号</Button>}
<Flex gap='middle' vertical justify={'center'} align={'center'}>
<QRCode
size={264}
value={'https://web.whatsapp.com/'}
status='scanned'
statusRender={customStatusRender}
/>
<Alert message='WhatsApp8618754124786' type='success' showIcon />
<Button type='primary'>退出</Button>
</Flex>
</Col>
</Row>
</>

@ -1,23 +1,117 @@
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(
"<p>Discover the best of the world with one of the <strong>best-rated tour companies for personalized travel</strong>. With over <strong>10,000+ reviews and a 98.8% 5-star rating</strong>, 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.</p>",
)
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 (
<>
<Modal
centered
okButtonProps={{
autoFocus: true,
htmlType: 'submit',
}}
width={800}
title={'图文'}
open={isSnippetModalOpen}
onCancel={() => setSnippetModalOpen(false)}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name='snippetForm'
form={snippetForm}
layout='vertical'
className='max-w-4xl'
onFinish={onSnippetFinish}
onFinishFailed={onSnippetFailed}
autoComplete='off'>
{dom}
</Form>
)}>
<Form.Item name='snippetId' className='hidden'>
<Input />
</Form.Item>
<Form.Item
label='标题'
name='title'
rules={[
{
required: true,
message: 'title required',
},
]}>
<Input />
</Form.Item>
<Form.Item
label='类型'
name='category'
rules={[
{
required: true,
message: 'title required',
},
]}>
<Select>
<Select.Option value="收款">收款</Select.Option>
<Select.Option value="西藏">西藏</Select.Option>
<Select.Option value="催信">催信</Select.Option>
<Select.Option value="提醒">提醒</Select.Option>
</Select>
</Form.Item>
<Form.Item
label='内容'
name='content'
rules={[
{
required: true,
message: 'content required',
},
]}>
<LexicalEditorInput />
</Form.Item>
</Modal>
<Space direction='vertical' size='large' style={{ width: '100%' }}>
<Form
layout={'inline'}
@ -25,11 +119,12 @@ function SnippetList() {
initialValues={{
layout: 'inline',
}}>
<Form.Item label='所有者'>
<Input placeholder='placeholder' />
<Select className='!w-40' options={ownerList} />
</Form.Item>
<Form.Item label='类别'>
<Input placeholder='placeholder' />
<Select className='!w-40' options={typeList} />
</Form.Item>
<Form.Item label='状态'>
<Input placeholder='placeholder' />
@ -38,18 +133,32 @@ function SnippetList() {
<Input placeholder='placeholder' />
</Form.Item>
<Form.Item>
<Button type='primary' onClick={() => {
<Button
type='primary'
onClick={() => {
console.info(editorRef.current)
const editorStateJSONString = '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"We are ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"expert in customizing, and best-rated in delivering personalized Asia exploration","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":", ensured by ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"our company-managed local services across Asia","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". The proof is our over ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"10,000 reviews with 98.8% 5-star ratings","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" on the most reputable platforms like ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"TripAdvisor and Trustpilot","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"start","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'
const editorStateJSONString =
'{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"We are ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"expert in customizing, and best-rated in delivering personalized Asia exploration","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":", ensured by ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"our company-managed local services across Asia","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":". The proof is our over ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"10,000 reviews with 98.8% 5-star ratings","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":" on the most reputable platforms like ","type":"text","version":1},{"detail":0,"format":1,"mode":"normal","style":"","text":"TripAdvisor and Trustpilot","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":".","type":"text","version":1}],"direction":"ltr","format":"start","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'
const editorState = editorRef.current.parseEditorState(editorStateJSONString);
const editorState = editorRef.current.parseEditorState(
editorStateJSONString,
)
editorRef.current.setEditorState(editorState)
}}>搜索</Button>
}}>
搜索
</Button>
</Form.Item>
<Form.Item>
<Button onClick={() => {
console.info(JSON.stringify(editorRef.current.getEditorState()))
}}>新增</Button>
<Button
onClick={() => {
snippetForm.setFieldsValue({
title: 'Title....',
content: editorContent
})
setSnippetModalOpen(true)
}}>
新增
</Button>
</Form.Item>
</Form>
<Row gutter={16}>
@ -64,7 +173,9 @@ function SnippetList() {
'Highlights Travel Family Loyalty Club',
]}
renderItem={(item) => (
<List.Item className='hover:bg-stone-50' onClick={(e) => {
<List.Item
className='hover:bg-stone-50'
onClick={(e) => {
console.info(item)
setEditorContent('<strong>' + item + '</strong>')
}}>
@ -81,14 +192,54 @@ function SnippetList() {
)}
/>
</Col>
<Col span={16} className='border-solid border rounded border-gray-300'>
<pre className='whitespace-pre-wrap break-words'>
<p>Discover China with the <strong>award-winning</strong> and <strong>best-rated</strong> tour company for <strong>personalized travel in China</strong>. Honored as <strong>China's Leading Tour Operator</strong> by the <strong>World Travel Awards</strong>, we boast <strong>10,000+ reviews</strong> and a remarkable <strong>98.8% 5-star rating</strong>. 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!</p>
<Col span={16}>
<div className='border-solid border rounded border-gray-300'>
<Space
direction='vertical'
size='middle'
style={{
display: 'flex',
}}>
<pre className='whitespace-pre-wrap break-words ps-6 pe-6'>
<p>
Discover China with the <strong>award-winning</strong> and{' '}
<strong>best-rated</strong> tour company for{' '}
<strong>personalized travel in China</strong>. Honored as{' '}
<strong>China is Leading Tour Operator</strong> by the{' '}
<strong>World Travel Awards</strong>, we boast{' '}
<strong>10,000+ reviews</strong> and a remarkable{' '}
<strong>98.8% 5-star rating</strong>. 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!
</p>
</pre>
<LexicalEditor {...{ isRichText: true, editorRef }} onChange={() => {console.info('onChange')}} initialValue={editorContent} />
<Flex gap='middle' justify='flex-end' wrap className='p-6'>
<Button
onClick={() => {
setSnippetModalOpen(true)
}}>
编辑
</Button>
<Button
onClick={() => {
//
}}>
复制
</Button>
<Button
danger
onClick={() => {
//
}}>
删除
</Button>
</Flex>
</Space>
</div>
</Col>
</Row>
</Space>
</>
)

@ -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 (
<Flex justify='center' align='center' gap='middle' vertical>
<Result
status='success'
title='扫码成功'
subTitle='正在获取你的权限'
extra={[
<Spin size='small' />
]}
/>
</Flex>
)
} else if (loginStatus === 302) {
navigate('/')
} else if (loginStatus === 403) {
return (
<Flex justify='center' align='center' gap='middle' vertical>
<Result
status='403'
title='403'
subTitle='你没有绑定钉钉账号,无法登陆。'
/>
</Flex>
)
} else {
return (
<Flex justify='center' align='center' gap='middle' vertical>
<Title level={4}>使用钉钉扫码</Title>
<div id='qrCodeContainer' style={{ border: '12px solid rgba(5, 5, 5, 0.06)', borderRadius: '8px' }}></div>
<div
id='qrCodeContainer'
style={{
border: '12px solid rgba(5, 5, 5, 0.06)',
borderRadius: '8px',
}}></div>
<span>钉钉 authCode: {result}</span>
<Input placeholder='placeholder' ref={codeRef} />
<Button
danger
onClick={() => {
console.info(codeRef.current.nativeElement.value)
login(codeRef.current.nativeElement.value)
}}>
登录
</Button>
</Flex>
)
}
}
export default QRCode
Loading…
Cancel
Save