Merge remote-tracking branch 'origin/main' into dev/chat

dev/chat
Lei OT 2 years ago
commit dd267e1223

@ -1,17 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { AuthContext } from '@/stores/AuthContext'
import { ThemeContext } from '@/stores/ThemeContext'
import AuthApp from '@/views/AuthApp'
import Standlone from '@/views/Standlone'
import OrderFollow from '@/views/OrderFollow'
import ChatHistory from '@/views/ChatHistory'
import SalesManagement from '@/views/SalesManagement'
import DingdingQRCode from '@/views/DingdingQRCode'
import DingdingQRCode from '@/views/dingding/QRCode'
import DingdingCallback from '@/views/dingding/Callback'
import AccountProfile from '@/views/AccountProfile'
import ErrorPage from '@/components/ErrorPage'
import Conversations from '@/views/Conversations/ChatWindow'
import MobileLogin from '@/views/mobile/Login'
import useAuthStore from '@/stores/AuthStore'
import '@/assets/index.css'
@ -37,6 +38,14 @@ const router = createBrowserRouter([
element: <Standlone />,
children: [
{ path: 'dingding/qrcode', element: <DingdingQRCode /> },
{ path: 'dingding/callback', element: <DingdingCallback /> },
],
},
{
path: '/m',
element: <Standlone />,
children: [
{ path: 'login', element: <MobileLogin /> },
],
},
])

@ -1,7 +0,0 @@
import { createContext, useContext } from 'react'
export const AuthContext = createContext({})
export function useAuthContext() {
return useContext(AuthContext)
}

@ -1,5 +1,5 @@
import { LinkOutlined, MailOutlined, PhoneOutlined, UserOutlined, WhatsAppOutlined } from '@ant-design/icons'
import { Card, Flex, Select, Typography } from 'antd'
import { App, Card, Flex, Select, Typography, Empty, Button } from 'antd'
import { useEffect } from 'react'
import { useParams } from 'react-router-dom'
@ -7,6 +7,7 @@ import useOrderStore from '@/stores/OrderStore'
import QuotesHistory from './QuotesHistory'
const CustomerProfile = (() => {
const { notification } = App.useApp()
const { order_sn: order_sn } = useParams()
const { orderDetail, customerDetail, lastQuotation, quotationList,
fetchOrderDetail, setOrderPropValue
@ -19,67 +20,88 @@ const CustomerProfile = (() => {
let regularText = ''
if (orderDetail.buytime > 0) regularText = '(R' + orderDetail.buytime + ')'
return (
<div className=' divide-x-0 divide-y divide-dashed divide-gray-300 '>
<Card className='p-2 '
bordered={false}
title={orderDetail.order_no}
actions={[
<Select
style={{
width: '100%'
}}
variant='borderless'
onSelect={(value) => {
setOrderPropValue(order_sn, 'orderlabel', value)
}}
value={orderDetail.tags}
options={[
{ value: 0, label: '未设置', disabled: true, },
{ value: 240003, label: '重点' },
{ value: 240002, label: '次重点' },
{ value: 240001, label: '一般' }
]}
/>,
<Select
style={{
width: '100%'
}}
variant='borderless'
onSelect={(value) => {
setOrderPropValue(order_sn,'orderstatus', value)
}}
value={orderDetail.states}
options={[
{ value: 1, label: '新订单' },
{ value: 2, label: '报价中' },
{ value: 3, label: '以后联系' },
{ value: 4, label: '等待付订金' },
{ value: 5, label: '成行' },
{ value: 6, label: '丢失' },
{ value: 7, label: '取消' }
]}
/>
]}
>
<Flex gap={10}>
<Flex vertical={true} justify='space-between'>
<Typography.Text ><UserOutlined className=' pr-1' />{customerDetail.name + regularText}</Typography.Text>
<Typography.Text ><PhoneOutlined className=' pr-1' />{customerDetail.phone}</Typography.Text>
<Typography.Text ><MailOutlined className=' pr-1' />{customerDetail.email}</Typography.Text>
<Typography.Text ><WhatsAppOutlined className='pr-1' />{customerDetail.whatsapp_phone_number}</Typography.Text>
if (order_sn) {
return (
<div className=' divide-x-0 divide-y divide-dashed divide-gray-300 '>
<Card className='p-2 '
bordered={false}
title={orderDetail.order_no}
actions={[
<Select
style={{
width: '100%'
}}
variant='borderless'
onSelect={(value) => {
setOrderPropValue(order_sn, 'orderlabel', value)
}}
value={orderDetail.tags}
options={[
{ value: 0, label: '未设置', disabled: true, },
{ value: 240003, label: '重点' },
{ value: 240002, label: '次重点' },
{ value: 240001, label: '一般' }
]}
/>,
<Select
style={{
width: '100%'
}}
variant='borderless'
onSelect={(value) => {
setOrderPropValue(order_sn,'orderstatus', value)
}}
value={orderDetail.states}
options={[
{ value: 1, label: '新订单' },
{ value: 2, label: '报价中' },
{ value: 3, label: '以后联系' },
{ value: 4, label: '等待付订金' },
{ value: 5, label: '成行' },
{ value: 6, label: '丢失' },
{ value: 7, label: '取消' }
]}
/>
]}
>
<Flex gap={10}>
<Flex vertical={true} justify='space-between'>
<Typography.Text ><UserOutlined className=' pr-1' />{customerDetail.name + regularText}</Typography.Text>
<Typography.Text ><PhoneOutlined className=' pr-1' />{customerDetail.phone}</Typography.Text>
<Typography.Text ><MailOutlined className=' pr-1' />{customerDetail.email}</Typography.Text>
<Typography.Text ><WhatsAppOutlined className='pr-1' />{customerDetail.whatsapp_phone_number}</Typography.Text>
</Flex>
</Flex>
</Card>
<Flex vertical={true} className='p-2 '>
<Typography.Text strong>最新报价</Typography.Text>
<p className='m-0 py-2 line-clamp-2 '><a target='_blank' href={lastQuotation.letterurl}><LinkOutlined />&nbsp;{lastQuotation.lettertitle}</a></p>
<Flex justify={'space-between'} >
<QuotesHistory dataSource={quotationList} />
</Flex>
</Flex>
</Card>
<Flex vertical={true} className='p-2 '>
<Typography.Text strong>最新报价</Typography.Text>
<p className='m-0 py-2 line-clamp-2 '><a target='_blank' href={lastQuotation.letterurl}><LinkOutlined />&nbsp;{lastQuotation.lettertitle}</a></p>
<Flex justify={'space-between'} >
<QuotesHistory dataSource={quotationList} />
</Flex>
</Flex>
<p className='p-2 shadow-inner overflow-auto m-0 break-words whitespace-pre-wrap ' dangerouslySetInnerHTML={{__html: orderDetail.order_detail}}></p>
</div>
)
<p className='p-2 shadow-inner overflow-auto m-0 break-words whitespace-pre-wrap ' dangerouslySetInnerHTML={{__html: orderDetail.order_detail}}></p>
</div>
)
} else {
return (
<Empty
description={
<span>
暂无相关订单
</span>
}
>
<Button type='primary' onClick={() => {
notification.info({
message: '温馨提示',
description: '功能还在开发中,敬请期待',
placement: 'top',
duration: 60,
})
}}>现在关联</Button>
</Empty>
)
}
})
export default CustomerProfile

@ -1,5 +1,5 @@
import {
App, Badge, Button, DatePicker, Tabs, Flex, Form, Input,
App, Badge, Button, DatePicker, Tabs, Flex, Form, Input, Empty,
Radio, Row, Col, Select, Space, Switch, Table, Tag, Tooltip
} from 'antd'
import { InfoCircleTwoTone, MessageTwoTone, PhoneTwoTone, MailTwoTone, WhatsAppOutlined } from '@ant-design/icons'
@ -197,8 +197,8 @@ function OrderGroupTable({ formValues }) {
return (
<Space>
{text}
{statusIcon}
{text}
</Space>
)
}
@ -320,21 +320,16 @@ function OrderGroupTable({ formValues }) {
}
)
})
return (<Tabs defaultActiveKey={0} items={collapseItems} />)
// return (<Collapse bordered={false} defaultActiveKey={0} items={collapseItems} />)
return (<Conditional
condition={orderList.length > 0}
whenTrue={<Tabs defaultActiveKey={0} items={collapseItems} />}
whenFalse={<Empty />}
/>)
}
function OrderFollow() {
// const [advanceChecked, toggleAdvance] = useState(false)
// const [formValues, setFormValues] = useState({
// type: 'today',
// orderStatus: '',
// orderNumber: '',
// orderLabel: '',
// startDate: ''
// })
const [formValues, setFormValues] = useFormStore(useShallow((state) => [state.orderFollowForm, state.setOrderFollowForm]));
const [advanceChecked, toggleAdvance] = useFormStore(useShallow((state) => [state.orderFollowAdvanceChecked, state.setOrderFollowAdvanceChecked]));

@ -1,26 +1,14 @@
import '@/assets/App.css'
import AppLogo from '@/assets/logo-gh.png'
import useAuthStore from '@/stores/AuthStore'
import { useThemeContext } from '@/stores/ThemeContext'
import { App as AntApp, Col, ConfigProvider, Empty, Layout, Row, Typography, theme } from 'antd'
import { NavLink, Outlet, useHref, useNavigate } from 'react-router-dom'
import { useEffect } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
const { Header, Footer, Content } = Layout
const { Title } = Typography
function Standlone() {
const {colorPrimary, borderRadius} = useThemeContext()
const { loginUser } = useAuthStore()
const navigate = useNavigate()
const href = useHref()
useEffect(() => {
// /p...
if (loginUser.userId > 0) {
navigate('/');
}
}, [href])
const {
token: { colorBgContainer },

@ -0,0 +1,72 @@
import useAuthStore from '@/stores/AuthStore'
import { isNotEmpty } from '@/utils/commons'
import { Flex, Result, Spin, Typography } from 'antd'
import { useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
const { Title } = Typography
// https://open.dingtalk.com/document/orgapp/obtain-identity-credentials#title-4up-u8w-5ug
// OAuth
// https://login.dingtalk.com/oauth2/auth?redirect_uri=https%3A%2F%2Fsales.mycht.cn%2Fp%2Fdingding%2Fcallback&response_type=code&client_id=dingwgdx6emlxr3fcrg8&scope=openid&state=global-saels&prompt=consent
function Callback() {
const navigate = useNavigate()
const { loginStatus, login, logout, setLoginStatus } = useAuthStore()
const urlSearch = new URLSearchParams(location.search)
const authCode = urlSearch.get('authCode')
const state = urlSearch.get('state')
const error = urlSearch.get('error')
useEffect (() => {
if (isNotEmpty(authCode) && state === 'global-saels') {
login(authCode)
} else {
console.error('error: ' + error)
}
}, [])
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('/m/login')
} 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>
<Result
status='success'
title='登陆成功'
subTitle='正在获取你的权限'
extra={[
<Spin size='small' />
]}
/>
</Flex>
)
}
}
export default Callback

@ -1,17 +1,24 @@
import useAuthStore from '@/stores/AuthStore'
import { Flex, Result, Spin, Typography } from 'antd'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useHref } from 'react-router-dom'
const { Title } = Typography
// https://open.dingtalk.com/document/orgapp/tutorial-obtaining-user-personal-information#title-qpi-0qv-anm
function DingdingQRCode() {
function QRCode() {
const navigate = useNavigate()
const href = useHref()
const { loginStatus, login, logout, setLoginStatus } = useAuthStore()
const { loginStatus, loginUser, login, logout, setLoginStatus } = useAuthStore()
useEffect(() => {
if (loginUser.userId > 0) {
navigate('/');
}
}, [href])
useEffect (() => {
if (location.search === '?out') {
@ -83,4 +90,4 @@ function DingdingQRCode() {
}
}
export default DingdingQRCode
export default QRCode

@ -0,0 +1,85 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react';
import { Avatar, List, Button, Input, Layout, Select, DatePicker, Form, Spin } from 'antd';
import { ChatItem, MessageBox } from 'react-chat-elements';
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions';
import { isEmpty } from '@/utils/utils';
import useFormStore from '@/stores/FormStore';
import { useShallow } from 'zustand/react/shallow';
import { RightOutlined } from '@ant-design/icons'
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions';
import SearchInput from '@/components/SearchInput';
const { Sider, Content, Header, Footer } = Layout;
const { Search } = Input;
const { RangePicker } = DatePicker;
const data = [
{
title: 'Tyler Dru Kelly',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: '15127570944',
},
{
title: 'Chhavi',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311112',
},
{
title: 'Nathan Posey',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311114',
},
{
title: 'Philip',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240312062',
},
{
title: 'Jeanne',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240313032',
},
{
title: 'Susan Puls',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240311213',
},
{
title: 'Ana Beatriz',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240312073',
},
{
title: 'Kathleen Anne Workman',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'HXY240104171',
},
]
function Login() {
return (
<>
<List
itemLayout='horizontal'
dataSource={data}
renderItem={(item, index) => (
<List.Item
actions={[<RightOutlined key='goto' />]}
>
<List.Item.Meta
avatar={<Avatar src={item.avatarUrl} />}
title={<a href='https://ant.design'>{item.title}</a>}
description={item.msgTime}
/>
</List.Item>
)}
/>
</>
);
}
export default Login;
Loading…
Cancel
Save