giao dien moi
This commit is contained in:
21576
package-lock.json
generated
21576
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,10 +32,10 @@
|
|||||||
"@types/node": "^12.20.7",
|
"@types/node": "^12.20.7",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
|
"http-proxy-middleware": "^0.19.1",
|
||||||
"react-app-rewired": "^2.1.8",
|
"react-app-rewired": "^2.1.8",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"web-vitals": "^1.1.1",
|
"web-vitals": "^1.1.1"
|
||||||
"http-proxy-middleware": "^0.19.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
|
|||||||
139
src/App.tsx
139
src/App.tsx
@@ -1,30 +1,30 @@
|
|||||||
import React, {createRef, useState, Fragment, FC} from 'react';
|
import React, { createRef, useState, Fragment, FC } from 'react';
|
||||||
import {Layout, Col, Row, Button, Image} from 'antd';
|
import { Layout, Col, Row, Button, Image } from 'antd';
|
||||||
import { EditOutlined, CloseOutlined } from '@ant-design/icons';
|
import { EditOutlined, CloseOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||||
import ConversationList from "@/components/ConversationList";
|
import ConversationList from "@/components/ConversationList";
|
||||||
import Chatbox from "@/components/Chatbox";
|
import Chatbox from "@/components/Chatbox";
|
||||||
import CustomerInfo from "@/components/CustomerInfo";
|
import CustomerInfo from "@/components/CustomerInfo";
|
||||||
import ActionTabs from "@/components/ActionTabs";
|
import ActionTabs from "@/components/ActionTabs";
|
||||||
import {HelpSideBar} from "@/components/Help";
|
import { HelpSideBar } from "@/components/Help";
|
||||||
import HeaderComponent from "@/components/HeaderComponent";
|
import HeaderComponent from "@/components/HeaderComponent";
|
||||||
|
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {AppState} from "@/store/typing";
|
import { AppState } from "@/store/typing";
|
||||||
import {UserInfo} from "@/typings/user";
|
import { UserInfo } from "@/typings/user";
|
||||||
import {actions} from "@/store/actions";
|
import { actions } from "@/store/actions";
|
||||||
|
|
||||||
import GlobalModal from '@/components/GlobalModal';
|
import GlobalModal from '@/components/GlobalModal';
|
||||||
import GlobalDrawer from "@/components/GlobalDrawer";
|
import GlobalDrawer from "@/components/GlobalDrawer";
|
||||||
import BadgeStatus from "@/components/BadgeStatus";
|
import BadgeStatus from "@/components/BadgeStatus";
|
||||||
import NetworkError from "@/components/Error/NetworkError";
|
import NetworkError from "@/components/Error/NetworkError";
|
||||||
|
|
||||||
import {ClientSettings} from "@/typings";
|
import { ClientSettings } from "@/typings";
|
||||||
|
|
||||||
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
|
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
|
||||||
import DashBoard from "@/components/DashBoard";
|
import DashBoard from "@/components/DashBoard";
|
||||||
|
|
||||||
import {user_list} from "@/test/test_state";
|
import { user_list } from "@/test/test_state";
|
||||||
|
import { DEFAULT_AVATAR } from "@/config";
|
||||||
import '@/styles/app.css';
|
import '@/styles/app.css';
|
||||||
|
|
||||||
|
|
||||||
@@ -33,11 +33,11 @@ const { Header, Content, Sider } = Layout;
|
|||||||
const WINDOW_HEIGHT = global.window.innerHeight;
|
const WINDOW_HEIGHT = global.window.innerHeight;
|
||||||
|
|
||||||
|
|
||||||
const UserNameForm = ({user}: {user: UserInfo | null}) => {
|
const UserNameForm = ({ user }: { user: UserInfo | null }) => {
|
||||||
const [openForm, setFormOpen] = useState<boolean>(false);
|
const [openForm, setFormOpen] = useState<boolean>(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
if( ! user ) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,17 +50,24 @@ const UserNameForm = ({user}: {user: UserInfo | null}) => {
|
|||||||
}));
|
}));
|
||||||
setFormOpen(false);
|
setFormOpen(false);
|
||||||
}
|
}
|
||||||
|
const avatar = user.avatar || DEFAULT_AVATAR;
|
||||||
if(!openForm) {
|
if (!openForm) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="cuscontentCenterHeader">
|
||||||
|
<div className="avatar-cus">
|
||||||
|
<img className="conversation-photo" src={avatar} alt="conversation" />
|
||||||
<BadgeStatus online={user.online || false}>
|
<BadgeStatus online={user.online || false}>
|
||||||
{user.name}
|
</BadgeStatus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-cus">
|
||||||
|
<div className="name">{user.name}</div>
|
||||||
|
<div className="address">{user.location}</div>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
user.typing && <Image preview={false} style={{height:20}} src={TYPING_ANIMATION_IMAGE} />
|
user.typing && <Image preview={false} style={{ height: 20 }} src={TYPING_ANIMATION_IMAGE} />
|
||||||
}
|
}<div className="cuscontentCenterHeaderEdit">( <EditOutlined onClick={() => setFormOpen(true)} title={'Thay đổi tên'} /> )</div>
|
||||||
</BadgeStatus> ( <EditOutlined onClick={() => setFormOpen(true)} title={'Thay đổi tên'} /> )
|
</div>
|
||||||
</Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,86 +79,118 @@ const UserNameForm = ({user}: {user: UserInfo | null}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getUserById(user_list: UserInfo[], user_id: string) : UserInfo | null {
|
function getUserById(user_list: UserInfo[], user_id: string): UserInfo | null {
|
||||||
let filtered_list = user_list.filter(user => user.id === user_id);
|
let filtered_list = user_list.filter(user => user.id === user_id);
|
||||||
return filtered_list.length > 0 ? filtered_list[0] : null;
|
return filtered_list.length > 0 ? filtered_list[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShowUserSpace = ({openHelp}: {openHelp: () => any}) => {
|
||||||
const ShowUserSpace = () => {
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
|
|
||||||
const chatting_with_user = useSelector((state: AppState) => state.chatting_with_user );
|
const chatting_with_user = useSelector((state: AppState) => state.chatting_with_user);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
let chatting_user_info = getUserById(user_list, chatting_with_user);
|
let chatting_user_info = getUserById(user_list, chatting_with_user);
|
||||||
if( ! chatting_user_info && user_list.length > 0) {
|
if (!chatting_user_info && user_list.length > 0) {
|
||||||
// auto show the first user in the list if there
|
// auto show the first user in the list if there
|
||||||
dispatch(actions.chatWithUser(user_list[0].id));
|
dispatch(actions.chatWithUser(user_list[0].id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// else show the dashboard
|
// else show the dashboard
|
||||||
if( ! chatting_user_info ) {
|
if (!chatting_user_info) {
|
||||||
return <DashBoard />
|
return <DashBoard />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div>
|
<div className="contentCenterHeader">
|
||||||
<UserNameForm user={chatting_user_info} />
|
<UserNameForm user={chatting_user_info} />
|
||||||
|
<div onClick={openHelp} className="contentCenterHeaderAdmin">
|
||||||
|
<span className="h-title">Trợ giúp admin</span>
|
||||||
|
<AppstoreOutlined className="h-icon" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="contentCenterCt">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={12}>
|
<Col className="contentCenterCtColLeft" span={12}>
|
||||||
{/* add key to force to component to remount when user id change to simplify the component's code*/}
|
{/* add key to force to component to remount when user id change to simplify the component's code*/}
|
||||||
<Chatbox key={chatting_user_info.id} user_info={chatting_user_info} />
|
<Chatbox key={chatting_user_info.id} user_info={chatting_user_info} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12} className="scrollable" style={{height: 600, overflow: 'auto'}}>
|
<Col span={12} className="contentCenterCtColRight">
|
||||||
<h3 style={{fontSize:16, fontWeight:'bold'}}>Lựa chọn</h3>
|
<div className="contentCenterCtColRightItem">
|
||||||
|
<h3 className="contentCenterCtColRightTitle" style={{ fontSize: 16, fontWeight: 'bold' }}>Lựa chọn</h3>
|
||||||
|
|
||||||
<ActionTabs customer_id={chatting_user_info.id} />
|
<ActionTabs customer_id={chatting_user_info.id} />
|
||||||
|
</div>
|
||||||
<h3 style={{fontSize:16, fontWeight:'bold'}}>Thông tin khách hàng</h3>
|
<div className="contentCenterCtColRightItem">
|
||||||
|
<h3 className="contentCenterCtColRightTitle" style={{ fontSize: 16, fontWeight: 'bold' }}>Thông tin khách hàng</h3>
|
||||||
|
|
||||||
<CustomerInfo user_info={chatting_user_info} />
|
<CustomerInfo user_info={chatting_user_info} />
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const App: FC<{client_setting: ClientSettings}> = ({client_setting}) => {
|
const App: FC<{ client_setting: ClientSettings }> = ({ client_setting }) => {
|
||||||
|
|
||||||
const HEADER_HEIGHT = 70;
|
const HEADER_HEIGHT = 70;
|
||||||
const [closeHelp, setHelpClose] = useState<boolean>(false);
|
const [closeHelp, setHelpClose] = useState<boolean>(false);
|
||||||
const LAYOUT_CLOSED = closeHelp ? {width:1200, marginLeft:'auto', marginRight: 'auto'} : {marginTop: HEADER_HEIGHT, height: WINDOW_HEIGHT - HEADER_HEIGHT, overflow: 'auto'};
|
const LAYOUT_CLOSED = closeHelp ? { width: 1200, marginLeft: 'auto', marginRight: 'auto' } : { marginTop: HEADER_HEIGHT, height: WINDOW_HEIGHT - HEADER_HEIGHT, overflow: 'auto' };
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout className={'box-chatboard'}>
|
||||||
<Header style={{ position: 'fixed', zIndex: 1, width: '100%' }}>
|
<Header style={{ width: '100%' }}>
|
||||||
<HeaderComponent />
|
<HeaderComponent />
|
||||||
</Header>
|
</Header>
|
||||||
<Layout style={LAYOUT_CLOSED}>
|
<Layout className={'content-chatboard'}>
|
||||||
|
<Row className={'content-chatboard-row'}>
|
||||||
<Sider width={250} className="scrollable">
|
<Col className={'content-chatboard-col'} span={4}>
|
||||||
|
<div className="contentLeft">
|
||||||
<ConversationList />
|
<ConversationList />
|
||||||
</Sider>
|
</div>
|
||||||
|
</Col>
|
||||||
<Content>
|
{
|
||||||
<ShowUserSpace />
|
windowWidth <= 1366 ?
|
||||||
|
<Col className={'content-chatboard-col'} span={20}>
|
||||||
|
<Content className="contentCenter">
|
||||||
|
<ShowUserSpace openHelp={() => setHelpClose(true)} />
|
||||||
</Content>
|
</Content>
|
||||||
|
</Col>
|
||||||
|
:
|
||||||
|
<Col className={'content-chatboard-col content-chatboard-col-big'} span={14}>
|
||||||
|
<Content className="contentCenter">
|
||||||
|
<ShowUserSpace openHelp={() => setHelpClose(true)} />
|
||||||
|
</Content>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!closeHelp &&
|
windowWidth <= 1366 ?
|
||||||
<Sider width={400} >
|
(
|
||||||
<h2>Trợ giúp <CloseOutlined onClick={() => setHelpClose(true)} title={'Đóng'} /> </h2>
|
closeHelp &&
|
||||||
|
<div className="fix-ab-ct-right" id="fix-ab-ct-right">
|
||||||
|
<div className="contentRight" >
|
||||||
|
<h2 className="contentRightHeader">Trợ giúp Admin <CloseOutlined onClick={() => setHelpClose(false)} title={'Đóng'} /> </h2>
|
||||||
<HelpSideBar />
|
<HelpSideBar />
|
||||||
</Sider>
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
:
|
||||||
|
<Col className={'content-chatboard-col'} span={6}>
|
||||||
|
<div className="contentRight" >
|
||||||
|
<h2 className="contentRightHeader">Trợ giúp Admin <CloseOutlined onClick={() => setHelpClose(false)} title={'Đóng'} /> </h2>
|
||||||
|
<HelpSideBar />
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
}
|
}
|
||||||
|
</Row>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<NetworkError />
|
<NetworkError />
|
||||||
|
|||||||
BIN
src/assets/images/avatar-admin.png
Normal file
BIN
src/assets/images/avatar-admin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/avatar-cus.png
Normal file
BIN
src/assets/images/avatar-cus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -54,8 +54,13 @@ const NoteForm = ({customer_id}: {customer_id: string|number}) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
form.submit();
|
form.submit();
|
||||||
}}
|
}}
|
||||||
|
className="taoghichu-input"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<div className="taoghichu-submit">
|
||||||
|
<button className="taoghichu-bt-update">Cập nhật</button>
|
||||||
|
<button className="taoghichu-bt-send">Gửi</button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,27 +12,27 @@ const { TabPane } = Tabs;
|
|||||||
const ActionTabs = ({customer_id}: {customer_id: string|number}) => {
|
const ActionTabs = ({customer_id}: {customer_id: string|number}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs key={customer_id} onChange={(key) => null } defaultActiveKey={'note'} type="card">
|
<Tabs className="tab-content-mes" key={customer_id} onChange={(key) => null } defaultActiveKey={'note'} type="card">
|
||||||
|
|
||||||
<TabPane tab="Tạo ghi chú" key="note">
|
<TabPane className="formTaoGhiChu" tab="Tạo ghi chú" key="note">
|
||||||
<Suspense fallback={<Loading />} >
|
<Suspense fallback={<Loading />} >
|
||||||
<CreateNoteComponent customer_id={customer_id}/>
|
<CreateNoteComponent customer_id={customer_id}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab="Tạo hỗ trợ" key="ticket">
|
<TabPane className="formTaoHoTro" tab="Tạo hỗ trợ" key="ticket">
|
||||||
<Suspense fallback={<Loading />} >
|
<Suspense fallback={<Loading />} >
|
||||||
<CreateSupportComponent customer_id={customer_id}/>
|
<CreateSupportComponent customer_id={customer_id}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab="Tạo đơn hàng" key="order">
|
<TabPane className="formTaoDonHang" tab="Tạo đơn hàng" key="order">
|
||||||
<Suspense fallback={<Loading />} >
|
<Suspense fallback={<Loading />} >
|
||||||
<CreateOrderComponent customer_id={customer_id}/>
|
<CreateOrderComponent customer_id={customer_id}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab="Tag" key="tag">
|
<TabPane className="formTag" tab="Tag" key="tag">
|
||||||
<Suspense fallback={<Loading />} >
|
<Suspense fallback={<Loading />} >
|
||||||
<CreateTag customer_id={customer_id}/>
|
<CreateTag customer_id={customer_id}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -39,7 +39,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-list-container {
|
.message-list-container {
|
||||||
padding: 10px;
|
padding: 20px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list-all {
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
padding: 20px 0px;
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
@@ -106,3 +114,123 @@
|
|||||||
border-top-right-radius: 2px;
|
border-top-right-radius: 2px;
|
||||||
border-bottom-right-radius: 2px;
|
border-bottom-right-radius: 2px;
|
||||||
}
|
}
|
||||||
|
.contentCenterCt .message-list::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterCt .message-list::-webkit-scrollbar-track {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterCt .message-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #C4C4C4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list .time-line-chat {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list .time-line-chat::after {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #F1EFEF;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0px;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list .time-line-chat span {
|
||||||
|
padding: 0px 10px;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: #676767;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.contentCenterCt .message-list .message {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.mes-content {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.mes-content .mes-contentLeft {
|
||||||
|
width: 36px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.mes-content .mes-contentLeft .mes-img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.mes-content .mes-contentLeft .mes-img img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.mes-content .mes-content-right {
|
||||||
|
width: calc(100% - 46px);
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.mes-content .mes-content-right .mes-content-header {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.mes-content .mes-content-right .mes-content-header .mes-contentName {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.mes-content .mes-content-right .mes-content-header .mes-content-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #A2A2A2;
|
||||||
|
}
|
||||||
|
.mes-content .mes-content-right .bubble-container {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.message .bubble-container .bubble {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
padding: 0px;
|
||||||
|
background: none !important;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000 !important;
|
||||||
|
text-align: left;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
.chatBottomContent {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
border-top: solid 1px #F1EFEF;
|
||||||
|
padding: 10px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.chatBottomContent textarea {
|
||||||
|
height: 40px !important;
|
||||||
|
min-height: 40px !important;
|
||||||
|
border: solid 1px #707070;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.chatBottomContent span.bt-mes-bt {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #858585;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.chatBottomContent span.bt-mes-bt span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import React, {Component, createRef, Fragment} from "react";
|
import React, { Component, createRef, Fragment } from "react";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {Dispatch} from "redux";
|
import { Dispatch } from "redux";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
import {UserInfo} from "@/typings/user";
|
import { UserInfo } from "@/typings/user";
|
||||||
import {ChatboxTextMessage} from "@/typings/message.d";
|
import { ChatboxTextMessage } from "@/typings/message.d";
|
||||||
import {sendTextMessageToServer} from "@/lib/messaging";
|
import { sendTextMessageToServer } from "@/lib/messaging";
|
||||||
import {getCurrentUTCTimestamp} from "@/lib/utils";
|
import { getCurrentUTCTimestamp } from "@/lib/utils";
|
||||||
import {getUserChatHistory} from "@/lib/api";
|
import { getUserChatHistory } from "@/lib/api";
|
||||||
import {AppState, NetworkingStatusType} from "@/store/typing";
|
import { AppState, NetworkingStatusType } from "@/store/typing";
|
||||||
import {actions} from "@/store/actions";
|
import { actions } from "@/store/actions";
|
||||||
import {NOTIFICATIONS} from "@/constant/text";
|
import { NOTIFICATIONS } from "@/constant/text";
|
||||||
import {getAdminInfo} from "@/lib/user";
|
import { getAdminInfo } from "@/lib/user";
|
||||||
import storage, {userChatHistoryStorageKey} from "@/lib/storage";
|
import storage, { userChatHistoryStorageKey } from "@/lib/storage";
|
||||||
|
|
||||||
import {MessageItem} from "./MessageItem";
|
import { MessageItem } from "./MessageItem";
|
||||||
import InputMessage from "./InputMessage";
|
import InputMessage from "./InputMessage";
|
||||||
import TypingNotification from "./TypingNotification";
|
import TypingNotification from "./TypingNotification";
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
this.loadingHistoryStatus = 'idle';
|
this.loadingHistoryStatus = 'idle';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(prevProps.chat_messages.length !== this.props.chat_messages.length) {
|
if (prevProps.chat_messages.length !== this.props.chat_messages.length) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,28 +103,28 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
|
|
||||||
console.log('Chatbox getChatHistory');
|
console.log('Chatbox getChatHistory');
|
||||||
|
|
||||||
const {user_info, chat_messages, network_connection, dispatch} = this.props;
|
const { user_info, chat_messages, network_connection, dispatch } = this.props;
|
||||||
|
|
||||||
// todo: 15-April-2021
|
// todo: 15-April-2021
|
||||||
// because 1 user can chat with many admin staff, we need to remove all old messages in storage (if exist) when user first chat with this staff
|
// because 1 user can chat with many admin staff, we need to remove all old messages in storage (if exist) when user first chat with this staff
|
||||||
if(network_connection === 'offline') {
|
if (network_connection === 'offline') {
|
||||||
// get for offline view only
|
// get for offline view only
|
||||||
const stored_messages: ChatboxTextMessage[] = await storage.get(userChatHistoryStorageKey(user_info.id)) || [];
|
const stored_messages: ChatboxTextMessage[] = await storage.get(userChatHistoryStorageKey(user_info.id)) || [];
|
||||||
console.log('stored_messages');
|
console.log('stored_messages');
|
||||||
console.log(stored_messages);
|
console.log(stored_messages);
|
||||||
dispatch(actions.addHistoryMessage({[user_info.id]: stored_messages}));
|
dispatch(actions.addHistoryMessage({ [user_info.id]: stored_messages }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( chat_messages.length === 0 || from_scroll ) {
|
if (chat_messages.length === 0 || from_scroll) {
|
||||||
|
|
||||||
let last_fetch = (chat_messages.length > 0) ? chat_messages[0].time : 0;
|
let last_fetch = (chat_messages.length > 0) ? chat_messages[0].time : 0;
|
||||||
|
|
||||||
this.setState({loadingHistory: true});
|
this.setState({ loadingHistory: true });
|
||||||
|
|
||||||
const old_messages = await getUserChatHistory({thread_id: user_info.id, last_fetch: last_fetch});
|
const old_messages = await getUserChatHistory({ thread_id: user_info.id, last_fetch: last_fetch });
|
||||||
|
|
||||||
this.setState({loadingHistory: false});
|
this.setState({ loadingHistory: false });
|
||||||
|
|
||||||
if (old_messages.length === 0) {
|
if (old_messages.length === 0) {
|
||||||
console.log('old_messages: this.noMoreHistoryMessage = true')
|
console.log('old_messages: this.noMoreHistoryMessage = true')
|
||||||
@@ -133,13 +133,13 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('dispatching historied messages');
|
console.log('dispatching historied messages');
|
||||||
dispatch(actions.addHistoryMessage({[user_info.id]: old_messages}));
|
dispatch(actions.addHistoryMessage({ [user_info.id]: old_messages }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom = () => {
|
scrollToBottom = () => {
|
||||||
// no scroll if user view history
|
// no scroll if user view history
|
||||||
if ( this.disableScrollBottom ) {
|
if (this.disableScrollBottom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
|
|
||||||
showNotification = () => {
|
showNotification = () => {
|
||||||
const { user_info, network_connection } = this.props;
|
const { user_info, network_connection } = this.props;
|
||||||
const NotiMessage = (txt: string, key: string|number) => (<div key={key}>{txt}</div>);
|
const NotiMessage = (txt: string, key: string | number) => (<div key={key}>{txt}</div>);
|
||||||
|
|
||||||
let list_messages: string[] = [];
|
let list_messages: string[] = [];
|
||||||
|
|
||||||
@@ -162,9 +162,9 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
list_messages.push(NOTIFICATIONS['user_offline']);
|
list_messages.push(NOTIFICATIONS['user_offline']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(list_messages.length > 0) {
|
if (list_messages.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div style={{fontStyle:'italic', fontSize:11, textAlign: 'center'}} >
|
<div style={{ fontStyle: 'italic', fontSize: 11, textAlign: 'center' }} >
|
||||||
{
|
{
|
||||||
list_messages.map((txt, index) => NotiMessage(txt, index))
|
list_messages.map((txt, index) => NotiMessage(txt, index))
|
||||||
}
|
}
|
||||||
@@ -181,18 +181,18 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
console.log('start handleChatboxScroll at ' + new Date().getSeconds());
|
console.log('start handleChatboxScroll at ' + new Date().getSeconds());
|
||||||
//console.log(event);
|
//console.log(event);
|
||||||
|
|
||||||
if(!event.target) {
|
if (!event.target) {
|
||||||
console.log('handleChatboxScroll: event.currentTarget');
|
console.log('handleChatboxScroll: event.currentTarget');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.noMoreHistoryMessage) {
|
if (this.noMoreHistoryMessage) {
|
||||||
console.log('handleChatboxScroll: noMoreHistoryMessage');
|
console.log('handleChatboxScroll: noMoreHistoryMessage');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// user is scrolling up, so disable bottom scrolling for user to read old messages without interruption
|
// user is scrolling up, so disable bottom scrolling for user to read old messages without interruption
|
||||||
if(this.trackLastScrollTop > event.target.scrollTop) {
|
if (this.trackLastScrollTop > event.target.scrollTop) {
|
||||||
this.disableScrollBottom = true;
|
this.disableScrollBottom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,18 +214,18 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
const { user_info, dispatch, network_connection } = this.props; // , admin_info, user_info
|
const { user_info, dispatch, network_connection } = this.props; // , admin_info, user_info
|
||||||
|
|
||||||
//const {typed_message} = this.state;
|
//const {typed_message} = this.state;
|
||||||
if(typed_message === '') {
|
if (typed_message === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset when user send new message
|
// reset when user send new message
|
||||||
if(this.disableScrollBottom) {
|
if (this.disableScrollBottom) {
|
||||||
this.disableScrollBottom = false;
|
this.disableScrollBottom = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// cannot send message when network_connection is offline
|
// cannot send message when network_connection is offline
|
||||||
// and restore message to the input
|
// and restore message to the input
|
||||||
if(network_connection === 'offline') {
|
if (network_connection === 'offline') {
|
||||||
console.log("network_connection =offline");
|
console.log("network_connection =offline");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
deliveryStatus: 0,
|
deliveryStatus: 0,
|
||||||
} as ChatboxTextMessage;
|
} as ChatboxTextMessage;
|
||||||
|
|
||||||
dispatch(actions.addCurrentMessage({[user_info.id] : [composed_message]}));
|
dispatch(actions.addCurrentMessage({ [user_info.id]: [composed_message] }));
|
||||||
|
|
||||||
// pass to networking layer to send to server
|
// pass to networking layer to send to server
|
||||||
let send_to = (user_info.online) ? [user_info.id, user_info.node].join('-').trim() : '';
|
let send_to = (user_info.online) ? [user_info.id, user_info.node].join('-').trim() : '';
|
||||||
@@ -249,11 +249,11 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoadingHistory = () => {
|
showLoadingHistory = () => {
|
||||||
const {loadingHistory} = this.state;
|
const { loadingHistory } = this.state;
|
||||||
|
|
||||||
if(loadingHistory) {
|
if (loadingHistory) {
|
||||||
return (
|
return (
|
||||||
<div style={{fontStyle:'italic', fontSize:11, textAlign: 'center'}} >Loading ...</div>
|
<div style={{ fontStyle: 'italic', fontSize: 11, textAlign: 'center' }} >Loading ...</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,13 +264,14 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const { network_connection, user_info, chat_messages} = this.props;
|
const { network_connection, user_info, chat_messages } = this.props;
|
||||||
const admin_info = getAdminInfo();
|
const admin_info = getAdminInfo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment >
|
<Fragment >
|
||||||
|
<div className={'message-list-all'}>
|
||||||
<div
|
<div
|
||||||
style={{height: 600, overflow: 'auto'}}
|
style={{ overflow: 'auto' }}
|
||||||
onScroll={this.handleChatboxScroll}
|
onScroll={this.handleChatboxScroll}
|
||||||
// onClick={listenForInChatAction}
|
// onClick={listenForInChatAction}
|
||||||
ref={this.scrollToHistoryDiv}
|
ref={this.scrollToHistoryDiv}
|
||||||
@@ -279,21 +280,33 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
|
|
||||||
<div className="message-list-container">
|
<div className="message-list-container">
|
||||||
|
|
||||||
{ this.showLoadingHistory() }
|
{this.showLoadingHistory()}
|
||||||
|
|
||||||
{
|
{
|
||||||
chat_messages.map((message, index) => <MessageItem key={message.time + '-' + index} is_me={admin_info.id === message.from} {...message} />)
|
chat_messages.map((message, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
index % 3 == 1 && <div className="time-line-chat"><span>15pm 20-05-2021</span></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ user_info.typing && <TypingNotification /> }
|
<MessageItem key={message.time + '-' + index} is_me={admin_info.id === message.from} {...message} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{user_info.typing && <TypingNotification />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ this.showNotification() }
|
{this.showNotification()}
|
||||||
|
|
||||||
<div ref={this.scrollBottomDiv} />
|
<div ref={this.scrollBottomDiv} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<InputMessage
|
<InputMessage
|
||||||
sendMessage={this.sendMessage}
|
sendMessage={this.sendMessage}
|
||||||
network_connection={network_connection}
|
network_connection={network_connection}
|
||||||
@@ -304,7 +317,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState, ownProps: {user_info: UserInfo}) => ({
|
const mapStateToProps = (state: AppState, ownProps: { user_info: UserInfo }) => ({
|
||||||
network_connection: state.network_connection,
|
network_connection: state.network_connection,
|
||||||
chat_messages: state.current_messages[ownProps.user_info.id] || [],
|
chat_messages: state.current_messages[ownProps.user_info.id] || [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className="chatBottomContent">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
// ref={this.inputRef}
|
// ref={this.inputRef}
|
||||||
value={typed_message}
|
value={typed_message}
|
||||||
@@ -100,7 +100,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
okText="Yes"
|
okText="Yes"
|
||||||
cancelText="No"
|
cancelText="No"
|
||||||
>
|
>
|
||||||
<span style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Forbid</span>
|
<span className="bt-mes-bt" style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Forbid</span>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
okText="Yes"
|
okText="Yes"
|
||||||
cancelText="No"
|
cancelText="No"
|
||||||
>
|
>
|
||||||
<span style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Dừng chat</span>
|
<span className="bt-mes-bt" style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Dừng chat</span>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
content={<PickAdminOnlineToTransfer />}
|
content={<PickAdminOnlineToTransfer />}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
>
|
>
|
||||||
<span style={{cursor: 'pointer'}}><FastForwardOutlined />Chuyển người khác</span>
|
<span className="bt-mes-bt" style={{cursor: 'pointer'}}><FastForwardOutlined />Chuyển người khác</span>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
ref={imageUploadWithPreviewRef}
|
ref={imageUploadWithPreviewRef}
|
||||||
auth={admin_info.jwt||''}
|
auth={admin_info.jwt||''}
|
||||||
// current_list={SAMPLE_UPLOAD_FILE}
|
// current_list={SAMPLE_UPLOAD_FILE}
|
||||||
uploadButton={<span title={'Upload file'} style={{cursor: 'pointer'}}><PaperClipOutlined /> Upload</span>}
|
uploadButton={<span className="bt-mes-bt" title={'Upload file'} style={{cursor: 'pointer'}}><PaperClipOutlined /> Upload</span>}
|
||||||
listType={'picture'}
|
listType={'picture'}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
@@ -151,7 +151,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import {isUrlImage, validURL} from "@/lib/validation";
|
import { isUrlImage, validURL } from "@/lib/validation";
|
||||||
import {maskExternalUrl, randomBetween, showUnixTime} from "@/lib/utils";
|
import { maskExternalUrl, randomBetween, showUnixTime } from "@/lib/utils";
|
||||||
import {ChatboxTextMessage} from "@/typings/message.d";
|
import { ChatboxTextMessage } from "@/typings/message.d";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { DEFAULT_AVATAR } from "@/config";
|
||||||
|
|
||||||
|
|
||||||
export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
export const MessageItem = (props: { is_me: boolean } & ChatboxTextMessage) => {
|
||||||
let {deliveryStatus, from, content, time, is_me } = props;
|
let { deliveryStatus, from, content, time, is_me } = props;
|
||||||
let time_in_second = Math.round(time / 1000);
|
let time_in_second = Math.round(time / 1000);
|
||||||
|
|
||||||
if( ! content ) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
let image = (isUrlImage(content)) ? content : '';
|
let image = (isUrlImage(content)) ? content : '';
|
||||||
let status_colors = {
|
let status_colors = {
|
||||||
@@ -18,24 +19,24 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
|||||||
};
|
};
|
||||||
let html;
|
let html;
|
||||||
|
|
||||||
if ( is_me ) from = 'me';
|
if (is_me) from = 'me';
|
||||||
|
|
||||||
if (from === 'bot') {
|
if (from === 'bot') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span dangerouslySetInnerHTML={{__html: content}} />
|
<span dangerouslySetInnerHTML={{ __html: content }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image !== '') {
|
if (image !== '') {
|
||||||
if(_isImageUploadedToOurServer(image)) {
|
if (_isImageUploadedToOurServer(image)) {
|
||||||
html = `<a href="${maskExternalUrl(image)}" title="Click để xem ảnh" target="_blank"><img src={image} alt={""}/></a>`
|
html = `<a href="${maskExternalUrl(image)}" title="Click để xem ảnh" target="_blank"><img src={image} alt={""}/></a>`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
||||||
<span dangerouslySetInnerHTML={{__html: html}} />
|
<span dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,19 +45,19 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
||||||
<span dangerouslySetInnerHTML={{__html: html}} />
|
<span dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
<div style={{color: 'red'}}>(Ảnh chưa kiểm định. Cần thận trọng khi xem)</div>
|
<div style={{ color: 'red' }}>(Ảnh chưa kiểm định. Cần thận trọng khi xem)</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(validURL(content)) {
|
if (validURL(content)) {
|
||||||
html = `<a href="${maskExternalUrl(content)}" title="Click để truy cập link này" target="_blank">${content}</a>`
|
html = `<a href="${maskExternalUrl(content)}" title="Click để truy cập link này" target="_blank">${content}</a>`
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
|
||||||
<span dangerouslySetInnerHTML={{__html: html}} />
|
<span dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
<div style={{color: 'red'}}>(Link chưa kiểm định. Cần thận trọng khi xem)</div>
|
<div style={{ color: 'red' }}>(Link chưa kiểm định. Cần thận trọng khi xem)</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
|||||||
// todo: remove this
|
// todo: remove this
|
||||||
let random_number = randomBetween(1, 100);
|
let random_number = randomBetween(1, 100);
|
||||||
let message_from_me = random_number % 2 === 0;
|
let message_from_me = random_number % 2 === 0;
|
||||||
|
const avatar = DEFAULT_AVATAR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
@@ -77,20 +79,27 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
|||||||
//`${endsSequence ? 'end' : ''}`
|
//`${endsSequence ? 'end' : ''}`
|
||||||
].join(' ')}>
|
].join(' ')}>
|
||||||
|
|
||||||
<div className="timestamp">
|
<div className="mes-content">
|
||||||
{from}: { showUnixTime(time_in_second) }
|
<div className="mes-contentLeft">
|
||||||
|
<span className="mes-img">
|
||||||
|
<img className="chat-photo" src={avatar} alt="chatavatar" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mes-content-right">
|
||||||
|
<div className="mes-content-header">
|
||||||
|
<span className="mes-contentName">Trần Hằng</span>
|
||||||
|
<span className="mes-content-time">11:20</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bubble-container">
|
<div className="bubble-container">
|
||||||
<div className="bubble" style={{color: (deliveryStatus !== 4) ? status_colors['sending'] : ''}} >
|
<div className="bubble" style={{ color: (deliveryStatus !== 4) ? status_colors['sending'] : '' }} >
|
||||||
{ content }
|
{content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
deliveryStatus === 5 && (<div style={{color: 'red'}}>(Lỗi xảy ra, tin nhắn chưa được gửi)</div>)
|
deliveryStatus === 5 && (<div style={{ color: 'red' }}>(Lỗi xảy ra, tin nhắn chưa được gửi)</div>)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
<div>deliveryStatus :{deliveryStatus} | sq:{props.sequence}</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -98,7 +107,7 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
|
|||||||
|
|
||||||
|
|
||||||
// format the display of each message on the chatbox
|
// format the display of each message on the chatbox
|
||||||
function _isImageUploadedToOurServer(image: string) : boolean{
|
function _isImageUploadedToOurServer(image: string): boolean {
|
||||||
//check images uploaded to chat server
|
//check images uploaded to chat server
|
||||||
let img_regex_1 = /(\/user_upload\/([0-9]{4}-[0-9]{2}-[0-9]{1,2})\/([a-z0-9_]+)\.(jpg|jpeg|gif|png))$/g;
|
let img_regex_1 = /(\/user_upload\/([0-9]{4}-[0-9]{2}-[0-9]{1,2})\/([a-z0-9_]+)\.(jpg|jpeg|gif|png))$/g;
|
||||||
//let img_regex_2 = /(\/file\.php\?f=([0-9]{4}-[0-9]{2}-[0-9]{1,2})&(amp;)?n=([a-z0-9_]+)\.(jpg|jpeg|gif|png))$/g;
|
//let img_regex_2 = /(\/file\.php\?f=([0-9]{4}-[0-9]{2}-[0-9]{1,2})&(amp;)?n=([a-z0-9_]+)\.(jpg|jpeg|gif|png))$/g;
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const CommentList = ({item_type, item_id} : {item_type: string, item_id?: string
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div >
|
<div className="form-commet-all">
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.conversation-list {
|
.conversation-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-search {
|
.conversation-search {
|
||||||
@@ -10,11 +11,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.conversation-search-input {
|
.conversation-search-input {
|
||||||
background: #f4f4f8;
|
border: none !important;
|
||||||
padding: 8px 10px;
|
font-size: 14px !important;
|
||||||
border-radius: 10px;
|
padding: 0px !important;
|
||||||
border: none;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-search-input::placeholder {
|
.conversation-search-input::placeholder {
|
||||||
@@ -61,3 +60,206 @@
|
|||||||
color: #CCCCCC;
|
color: #CCCCCC;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-search-customer {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: solid 1px rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .search-customer-form {
|
||||||
|
width: 100%;
|
||||||
|
border: solid 1px #707070;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .search-customer-form .search-input {
|
||||||
|
width: calc(100% - 36px);
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
padding: 0px 10px !important;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .search-customer-form button {
|
||||||
|
width: 36px;
|
||||||
|
float: right;
|
||||||
|
border: none;
|
||||||
|
background: #fff;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(65, 65, 65, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .filter-search-customer-form {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .filter-search-customer-form span {
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 0px 15px;
|
||||||
|
line-height: 30px;
|
||||||
|
border: solid 1px #575757;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .filter-search-customer-form span.select {
|
||||||
|
border: solid 1px #5DD498;
|
||||||
|
color: #5DD498;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-search-customer .filter-search-customer-form span:hover {
|
||||||
|
border: solid 1px #5DD498;
|
||||||
|
color: #5DD498;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-customer-chat {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
padding: 10px;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: justify;
|
||||||
|
-ms-flex-pack: justify;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-customer-chat .fcc-item span {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
padding: 10px;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item:hover {
|
||||||
|
background: rgba(71, 141, 220, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item.conversation-list-item-selected {
|
||||||
|
background: rgba(71, 141, 220, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .avatar-cus {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .avatar-cus img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
float: left;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .ant-badge-status {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: -7px;
|
||||||
|
right: 0px;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .ant-badge-status-text {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .avatar-cus .ant-badge-status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: solid 2px #001F3D;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #9A9A9A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .avatar-cus .ant-badge-status-dot.ant-badge-status-green {
|
||||||
|
background: #37EE14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .info-cus .name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .info-cus .address {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-list-item .count-mes {
|
||||||
|
padding: 0px 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
background: #C70000;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-customer-chat {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 147px);
|
||||||
|
float: left;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeaderEdit {
|
||||||
|
margin-top: -25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterHeader input {
|
||||||
|
width: 240px;
|
||||||
|
float: left;
|
||||||
|
border: solid 1px #001F3D;
|
||||||
|
border-radius: 0px;
|
||||||
|
outline: none;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterHeader .ant-btn {
|
||||||
|
height: 40px;
|
||||||
|
border: none !important;
|
||||||
|
background: #001F3D !important;
|
||||||
|
margin-left: -1px;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, {createRef, useState} from 'react';
|
import React, { createRef, useState } from 'react';
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import {Image, Input} from "antd";
|
import { Image, Input, Menu, Dropdown, Layout } from "antd";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import {AppState} from "@/store/typing";
|
import { AppState } from "@/store/typing";
|
||||||
import {UserInfo} from "@/typings/user";
|
import { UserInfo } from "@/typings/user";
|
||||||
import {actions} from "@/store/actions";
|
import { actions } from "@/store/actions";
|
||||||
import {DEFAULT_AVATAR} from "@/config";
|
import { DEFAULT_AVATAR } from "@/config";
|
||||||
import BadgeStatus from "@/components/BadgeStatus";
|
import BadgeStatus from "@/components/BadgeStatus";
|
||||||
|
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar';
|
||||||
@@ -15,13 +15,15 @@ import ToolbarButton from '../ToolbarButton';
|
|||||||
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
|
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
|
||||||
|
|
||||||
import './ConversationList.css';
|
import './ConversationList.css';
|
||||||
import {isFound} from "@/lib/vietnamese";
|
import { isFound } from "@/lib/vietnamese";
|
||||||
|
|
||||||
import {user_list} from "@/test/test_state";
|
import { user_list } from "@/test/test_state";
|
||||||
|
import { SearchOutlined, DownOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { Sider } = Layout;
|
||||||
|
|
||||||
//Note: this component has been well-tested, do NOT change it!!
|
//Note: this component has been well-tested, do NOT change it!!
|
||||||
const ConversationSearch = ({setKeyword} : {setKeyword: (keyword: string) => void}) => {
|
const ConversationSearch = ({ setKeyword }: { setKeyword: (keyword: string) => void }) => {
|
||||||
const search_wait_time = 300; // milli seconds
|
const search_wait_time = 300; // milli seconds
|
||||||
const inputRef = createRef<Input>();
|
const inputRef = createRef<Input>();
|
||||||
const startSearch = () => {
|
const startSearch = () => {
|
||||||
@@ -32,15 +34,24 @@ const ConversationSearch = ({setKeyword} : {setKeyword: (keyword: string) => voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="conversation-search">
|
<div className="box-search-customer">
|
||||||
|
<div className="search-customer-form">
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="search"
|
type="search"
|
||||||
className="conversation-search-input"
|
className="conversation-search-input search-input"
|
||||||
placeholder="Tìm kiếm"
|
placeholder="Tìm kiếm"
|
||||||
onChange={debounce(startSearch, search_wait_time)}
|
onChange={debounce(startSearch, search_wait_time)}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
|
<button type="submit">
|
||||||
|
<SearchOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="filter-search-customer-form">
|
||||||
|
<span className="select">Tât cả</span>
|
||||||
|
<span>Đang online</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,21 +79,86 @@ const ConversationListItem = (props: { user: UserInfo, unread: { total: number,
|
|||||||
return (
|
return (
|
||||||
<div className={containerClass} onClick={() => pickUserToChat()} title={unread.messages.join('\n')}>
|
<div className={containerClass} onClick={() => pickUserToChat()} title={unread.messages.join('\n')}>
|
||||||
|
|
||||||
<BadgeStatus online={user.online || false}>
|
<div className="avatar-cus">
|
||||||
<img className="conversation-photo" src={avatar} alt="conversation" />
|
<img className="conversation-photo" src={avatar} alt="conversation" />
|
||||||
|
<BadgeStatus online={user.online || false}>
|
||||||
</BadgeStatus>
|
</BadgeStatus>
|
||||||
|
|
||||||
<div className="conversation-info">
|
|
||||||
<h1 className="conversation-title">{ user.name } ({user.location})</h1>
|
|
||||||
<div>{user.id}</div>
|
|
||||||
{
|
|
||||||
user.typing && <Image preview={false} style={{height:20}} src={TYPING_ANIMATION_IMAGE} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="info-cus">
|
||||||
|
<div className="name">{user.name}</div>
|
||||||
|
<div className="address">{user.location}</div>
|
||||||
|
</div>
|
||||||
|
<div className="count-mes">5</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TongKhachHang = () => {
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
title={`Khách hàng (${user_list.length})`}
|
||||||
|
leftItems={[
|
||||||
|
<ToolbarButton key="cog" icon="ion-ios-cog" />
|
||||||
|
]}
|
||||||
|
rightItems={[
|
||||||
|
<ToolbarButton key="add" icon="ion-ios-add-circle-outline" />
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListSort = () => {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="/">
|
||||||
|
Tin nhắn đã xem
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="/">
|
||||||
|
Tin nhắn chưa xem
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListProvince = () => {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="/">
|
||||||
|
Hà Nội
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="/">
|
||||||
|
Hải phòng
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterSortConversationList = () => {
|
||||||
|
return (
|
||||||
|
<div className="filter-customer-chat">
|
||||||
|
<Dropdown overlay={ListSort}>
|
||||||
|
<a className="ant-dropdown-link ant-dropdown-link-style-new fcc-item" onClick={e => e.preventDefault()}>
|
||||||
|
<span>Tin nhắn mới <DownOutlined /></span>
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown overlay={ListProvince}>
|
||||||
|
<a className="ant-dropdown-link ant-dropdown-link-style-new fcc-item" onClick={e => e.preventDefault()}>
|
||||||
|
<span>Tỉnh thành <DownOutlined /></span>
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const ConversationList = () => {
|
const ConversationList = () => {
|
||||||
|
|
||||||
@@ -103,39 +179,30 @@ const ConversationList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//const no_unread = {total: 0, messages: []};
|
//const no_unread = {total: 0, messages: []};
|
||||||
const no_unread = {total: 2, messages: ['hello there', 'CÓ gì không thế?']};
|
const no_unread = { total: 2, messages: ['hello there', 'CÓ gì không thế?'] };
|
||||||
|
|
||||||
const getUserList = (query: string = '') => {
|
const getUserList = (query: string = '') => {
|
||||||
if(query === '') return user_list;
|
if (query === '') return user_list;
|
||||||
|
|
||||||
return user_list.filter((user) => isFound(query, user.name || '') );
|
return user_list.filter((user) => isFound(query, user.name || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
const un_read_message_per_user: {[key: string]: { total: number, messages: string[] } } = getUnreadMsgPerUser();
|
const un_read_message_per_user: { [key: string]: { total: number, messages: string[] } } = getUnreadMsgPerUser();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="conversation-list">
|
<div className="conversation-list">
|
||||||
|
|
||||||
<Toolbar
|
|
||||||
title={`Khách hàng (${user_list.length})`}
|
|
||||||
leftItems={[
|
|
||||||
<ToolbarButton key="cog" icon="ion-ios-cog" />
|
|
||||||
]}
|
|
||||||
rightItems={[
|
|
||||||
<ToolbarButton key="add" icon="ion-ios-add-circle-outline" />
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConversationSearch setKeyword={setKeyword} />
|
<ConversationSearch setKeyword={setKeyword} />
|
||||||
|
|
||||||
|
<FilterSortConversationList />
|
||||||
|
|
||||||
|
<div className="list-customer-chat">
|
||||||
{
|
{
|
||||||
getUserList(keyword).map(
|
getUserList(keyword).map(
|
||||||
(user: UserInfo) => <ConversationListItem key={user.id} user={user} unread={un_read_message_per_user[user.id] || no_unread} />
|
(user: UserInfo) => <ConversationListItem key={user.id} user={user} unread={un_read_message_per_user[user.id] || no_unread} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const BrowseHistoryComponent = React.lazy(() => import('./components/Brow
|
|||||||
const CustomerInfo = ({user_info}: {user_info: UserInfo}) => {
|
const CustomerInfo = ({user_info}: {user_info: UserInfo}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs type="card">
|
<Tabs className="tab-content-mes" type="card">
|
||||||
<TabPane tab="Thông tin" key="info">
|
<TabPane className="formThongTin" tab="Thông tin" key="info">
|
||||||
<Suspense fallback={<Loading />} >
|
<Suspense fallback={<Loading />} >
|
||||||
<CustomerInfoComponent user_info={user_info} key={'customer-info'}/>
|
<CustomerInfoComponent user_info={user_info} key={'customer-info'}/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, {Fragment} from "react";
|
import React, { Fragment } from "react";
|
||||||
import {useDispatch, useSelector, shallowEqual} from "react-redux";
|
import { useDispatch, useSelector, shallowEqual } from "react-redux";
|
||||||
import {actions} from "@/store/actions";
|
import { actions } from "@/store/actions";
|
||||||
import {AppState} from "@/store/typing";
|
import { AppState } from "@/store/typing";
|
||||||
import {getAdminInfo} from "@/lib/user";
|
import { getAdminInfo } from "@/lib/user";
|
||||||
import {Col, Row, Image, Menu, Dropdown} from "antd";
|
import { Col, Row, Image, Menu, Dropdown } from "antd";
|
||||||
import { DownOutlined, TeamOutlined, UserOutlined, QuestionOutlined } from '@ant-design/icons';
|
import { DownOutlined, TeamOutlined, UserOutlined, CaretDownFilled, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
import {MenuInfo} from "rc-menu/lib/interface";
|
import { MenuInfo } from "rc-menu/lib/interface";
|
||||||
|
import logo from '../../assets/images/logo.png'
|
||||||
|
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
@@ -34,7 +35,7 @@ const menu = (
|
|||||||
|
|
||||||
const HeaderComponent = () => {
|
const HeaderComponent = () => {
|
||||||
const admin_info = getAdminInfo();
|
const admin_info = getAdminInfo();
|
||||||
const {stats, admin_list} = useSelector((state: AppState) => (
|
const { stats, admin_list } = useSelector((state: AppState) => (
|
||||||
{
|
{
|
||||||
stats: state.stats,
|
stats: state.stats,
|
||||||
admin_list: state.admin_list,
|
admin_list: state.admin_list,
|
||||||
@@ -43,17 +44,26 @@ const HeaderComponent = () => {
|
|||||||
|
|
||||||
const current_stats: {
|
const current_stats: {
|
||||||
user_online: number,
|
user_online: number,
|
||||||
} = (stats !== '') ? JSON.parse(stats) : { user_online: 0};
|
} = (stats !== '') ? JSON.parse(stats) : { user_online: 0 };
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const setOpenModal = (component: string) => {
|
const setOpenModal = (component: string) => {
|
||||||
dispatch(actions.openGlobalModal({component, args: {}}))
|
dispatch(actions.openGlobalModal({ component, args: {} }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuClick = (e: MenuInfo) => {
|
const handleMenuClick = (e: MenuInfo) => {
|
||||||
setOpenModal(e.key + '')
|
setOpenModal(e.key + '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuAdminAccount = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="/">
|
||||||
|
Đăng xuất
|
||||||
|
</a>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
const TOTAL_ADMIN = admin_list.length;
|
const TOTAL_ADMIN = admin_list.length;
|
||||||
const ADMIN_ONLINE = admin_list.filter(admin => admin.online).length;
|
const ADMIN_ONLINE = admin_list.filter(admin => admin.online).length;
|
||||||
|
|
||||||
@@ -64,17 +74,25 @@ const HeaderComponent = () => {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
||||||
<Row className={'header'}>
|
<Row className={'header'}>
|
||||||
|
<Col span={4}>
|
||||||
|
<div className={'logo'}>
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
style={{ width: 154, height: 43 }}
|
||||||
|
preview={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
|
||||||
<Col span={20}>
|
<Menu className={'header-info'} mode="horizontal" onClick={handleMenuClick}>
|
||||||
|
<Menu.Item className={'header-info-click'} key="user" icon={<UserOutlined />} >
|
||||||
<Menu mode="horizontal" onClick={handleMenuClick}>
|
|
||||||
<Menu.Item key="user" icon={<UserOutlined />} >
|
|
||||||
Người dùng online ({current_stats.user_online})
|
Người dùng online ({current_stats.user_online})
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="admin" icon={<TeamOutlined />} >
|
<Menu.Item className={'header-info-click'} key="admin" icon={<TeamOutlined />} >
|
||||||
Quản trị viên online ({ADMIN_ONLINE}/{TOTAL_ADMIN})
|
Quản trị viên online ({ADMIN_ONLINE}/{TOTAL_ADMIN})
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="help" icon={<QuestionOutlined />}>
|
<Menu.Item className={'header-info-click'} key="help" icon={<QuestionCircleOutlined />}>
|
||||||
Trợ giúp
|
Trợ giúp
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -82,18 +100,19 @@ const HeaderComponent = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
|
<div className={'header-avatar'}>
|
||||||
<Dropdown overlay={menu}>
|
<Dropdown overlay={menuAdminAccount}>
|
||||||
<Fragment>
|
<a className="ant-dropdown-link ant-dropdown-link-style-new" onClick={e => e.preventDefault()}>
|
||||||
<Image
|
<Image
|
||||||
src={'https://chatngay-static.glee.vn/avatar/120_a1.jpg'}
|
src={'https://chatngay-static.glee.vn/avatar/120_a1.jpg'}
|
||||||
style={{width: 50, height: 50}}
|
style={{ width: 50, height: 50 }}
|
||||||
preview={false}
|
preview={false}
|
||||||
|
className={'header-avatar-img'}
|
||||||
/>
|
/>
|
||||||
<span className="name">{admin_info.name}</span>
|
<span className="name-admin">{admin_info.name} <CaretDownFilled /></span>
|
||||||
</Fragment>
|
</a>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,73 @@
|
|||||||
.header {
|
.header {
|
||||||
color: white;
|
color: white;
|
||||||
|
width: 100%;
|
||||||
|
height: 55px;
|
||||||
|
background: #001B34;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-header .ant-menu{
|
.ant-layout-header .ant-menu {
|
||||||
background: black;
|
background: #001B34;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.ant-layout-header .ant-menu {
|
||||||
|
line-height: 55px;
|
||||||
|
}
|
||||||
|
.ant-layout-header {
|
||||||
|
height: 55px !important;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
text-align: center;
|
||||||
|
height: 55px;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.header-info {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.header-info li {
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
.header-info-click {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #fff;
|
||||||
|
margin-right: 50px;
|
||||||
|
float: left;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.header-avatar {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.header-avatar-img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 10px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.name-admin {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.name-admin span {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.ant-dropdown-link-style-new {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, {useEffect, useState, Suspense} from "react";
|
import React, { useEffect, useState, Suspense } from "react";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import {HelpType, OpenHelpComponent} from "@/typings";
|
import { HelpType, OpenHelpComponent } from "@/typings";
|
||||||
import SearchBox from "./components/SearchBox";
|
import SearchBox from "./components/SearchBox";
|
||||||
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const ProductListComponent = React.lazy(() => import('./components/ProductList'));
|
const ProductListComponent = React.lazy(() => import('./components/ProductList'));
|
||||||
const ArticleListComponent = React.lazy(() => import('./components/ArticleList'));
|
const ArticleListComponent = React.lazy(() => import('./components/ArticleList'));
|
||||||
@@ -14,13 +15,13 @@ const WINDOW_HEIGHT = global.window.innerHeight;
|
|||||||
const HELP_HEIGHT = WINDOW_HEIGHT - 150;
|
const HELP_HEIGHT = WINDOW_HEIGHT - 150;
|
||||||
|
|
||||||
|
|
||||||
const HomeComponent = (props: {openComponent: (type: HelpType, params?: { [key: string] : any }) => void}) => {
|
const HomeComponent = (props: { openComponent: (type: HelpType, params?: { [key: string]: any }) => void }) => {
|
||||||
const [help, setHelp] = useState<{product: any[], article: any[]}>({product:[], article:[]});
|
const [help, setHelp] = useState<{ product: any[], article: any[] }>({ product: [], article: [] });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _getHelp = async () => {
|
const _getHelp = async () => {
|
||||||
let result = await api.get('help/home');
|
let result = await api.get('help/home');
|
||||||
if(result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
setHelp(result.data);
|
setHelp(result.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -28,55 +29,57 @@ const HomeComponent = (props: {openComponent: (type: HelpType, params?: { [key:
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="helpContent">
|
||||||
<div className={'scrollable'} style={{height: HELP_HEIGHT}}>
|
<div className={'helpContentRight'}>
|
||||||
|
|
||||||
<h3 style={{fontSize: 16}}>Sản phẩm: <span className={'a-link'} onClick={() => props.openComponent('product-list')}>Xem het</span></h3>
|
|
||||||
|
|
||||||
|
<h3 className="titleHelpRight">Sản phẩm: <span className={'a-link'} onClick={() => props.openComponent('product-list')}>Xem tất cả <ArrowRightOutlined /></span></h3>
|
||||||
|
<div className={'scrollable helpContentRightScroll'}>
|
||||||
<ProductListComponent
|
<ProductListComponent
|
||||||
defaultList={help.product.slice(0, 5)}
|
defaultList={help.product.slice(0, 5)}
|
||||||
openItem={(id) => props.openComponent('product-detail', {id})}
|
openItem={(id) => props.openComponent('product-detail', { id })}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<h3 style={{fontSize: 16}}>Kho kiến thức: <span className={'a-link'} onClick={() => props.openComponent('article-list')}>Xem het</span></h3>
|
</div>
|
||||||
|
<div className={'helpContentRight'}>
|
||||||
|
<h3 className="titleHelpRight">Kho kiến thức: <span className={'a-link'} onClick={() => props.openComponent('article-list')}>Xem tất cả <ArrowRightOutlined /></span></h3>
|
||||||
|
<div className={'scrollable helpContentRightScroll'}>
|
||||||
<ArticleListComponent
|
<ArticleListComponent
|
||||||
defaultList={help.article.slice(0, 5)}
|
defaultList={help.article.slice(0, 5)}
|
||||||
openItem={(id) => props.openComponent('article-detail', {id})}
|
openItem={(id) => props.openComponent('article-detail', { id })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const HelpSideBar = () => {
|
const HelpSideBar = () => {
|
||||||
|
|
||||||
const [component, setComponent] = useState<OpenHelpComponent>({type: 'home'});
|
const [component, setComponent] = useState<OpenHelpComponent>({ type: 'home' });
|
||||||
|
|
||||||
const openComponent = (type: HelpType, params?: { [key: string] : any }) => {
|
const openComponent = (type: HelpType, params?: { [key: string]: any }) => {
|
||||||
setComponent({type, params});
|
setComponent({ type, params });
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLoadedComponent = () => {
|
const getLoadedComponent = () => {
|
||||||
if(component.type === 'product-list') {
|
if (component.type === 'product-list') {
|
||||||
return <ProductListComponent params={component.params} openItem={(id) => openComponent('product-detail', {id})} />;
|
return <ProductListComponent params={component.params} openItem={(id) => openComponent('product-detail', { id })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(component.type === 'product-detail') {
|
if (component.type === 'product-detail') {
|
||||||
return <ProductDetailComponent id={component.params!.id} openHome={() => openComponent('home')} />;
|
return <ProductDetailComponent id={component.params!.id} openHome={() => openComponent('home')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(component.type === 'article-list') {
|
if (component.type === 'article-list') {
|
||||||
return <ArticleListComponent params={component.params} openItem={(id) => openComponent('article-detail', {id})} />;
|
return <ArticleListComponent params={component.params} openItem={(id) => openComponent('article-detail', { id })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(component.type === 'article-detail') {
|
if (component.type === 'article-detail') {
|
||||||
return <ArticleDetailComponent id={component.params!.id} openHome={() => openComponent('home')} />;
|
return <ArticleDetailComponent id={component.params!.id} openHome={() => openComponent('home')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(component.type === 'search') {
|
if (component.type === 'search') {
|
||||||
return <SearchComponent openComponent={openComponent} keyword={component.params!.q} />;
|
return <SearchComponent openComponent={openComponent} keyword={component.params!.q} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import {HelpItem} from "@/typings";
|
import { HelpItem } from "@/typings";
|
||||||
import Comment from "@/components/Comment";
|
import Comment from "@/components/Comment";
|
||||||
|
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const ArticleDetailComponent = ({ id, openHome }: { id?: string | number, openHome: () => void }) => {
|
||||||
const ArticleDetailComponent = ({id, openHome}: {id?: string|number, openHome: () => void}) => {
|
|
||||||
|
|
||||||
const [article_info, setArticle] = useState<HelpItem>({});
|
const [article_info, setArticle] = useState<HelpItem>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _getInfo = async () => {
|
const _getInfo = async () => {
|
||||||
let result = await api.get('article/info', {id});
|
let result = await api.get('article/info', { id });
|
||||||
if(result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
setArticle(result.data);
|
setArticle(result.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -21,13 +21,18 @@ const ArticleDetailComponent = ({id, openHome}: {id?: string|number, openHome: (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div><span className={'a-link'} onClick={openHome}>Back to home</span></div>
|
<div className="product-detail-header">
|
||||||
|
<div className="product-detail-header-left"><span className={'a-link'} onClick={openHome}><ArrowLeftOutlined /> Quay lại</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="product-detail-content article-detail-content">
|
||||||
|
|
||||||
<h3>{article_info.name}</h3>
|
<h3>{article_info.name}</h3>
|
||||||
|
|
||||||
<div>{article_info.summary}</div>
|
<div className="article-detail-content-ct">{article_info.summary}</div>
|
||||||
|
|
||||||
<Comment item_type={'product'} item_id={article_info.id} />
|
<Comment item_type={'product'} item_id={article_info.id} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { List, Pagination} from "antd";
|
import { List, Pagination, Select} from "antd";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {HelpItem} from "@/typings";
|
import {HelpItem} from "@/typings";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
|
import { CheckOutlined, CloseOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
|
||||||
const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: string|number) => void}) => {
|
const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: string|number) => void}) => {
|
||||||
@@ -12,6 +13,7 @@ const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: strin
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
|
className="Article-list"
|
||||||
itemLayout="horizontal"
|
itemLayout="horizontal"
|
||||||
dataSource={defaultList}
|
dataSource={defaultList}
|
||||||
renderItem={(item: HelpItem) => (
|
renderItem={(item: HelpItem) => (
|
||||||
@@ -27,6 +29,21 @@ const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
function handleChange(value: string) {
|
||||||
|
console.log(`selected ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectBoxCategory = () => {
|
||||||
|
return (
|
||||||
|
<div className="selectCategory">
|
||||||
|
<Select defaultValue="Danh mục" style={{ width: 120 }} onChange={handleChange}>
|
||||||
|
<Option value="1">Hướng dẫn</Option>
|
||||||
|
<Option value="2">Cài đặt</Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const FullListComponent = (props: {openItem: (id?: string|number) => void, params?: {[key: string]: any}}) => {
|
const FullListComponent = (props: {openItem: (id?: string|number) => void, params?: {[key: string]: any}}) => {
|
||||||
|
|
||||||
@@ -53,8 +70,15 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="help-full-content">
|
||||||
|
<div className="product-list-head">
|
||||||
|
<div className="product-list-head-Left"><span className={'a-link'} onClick={() => alert('về trang chủ')}><ArrowLeftOutlined />Quay lại</span></div>
|
||||||
|
<div className="product-list-head-right">
|
||||||
|
<SelectBoxCategory />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<List
|
<List
|
||||||
|
className="help-list-full article-list"
|
||||||
itemLayout="horizontal"
|
itemLayout="horizontal"
|
||||||
dataSource={item_list.current}
|
dataSource={item_list.current}
|
||||||
renderItem={(item: HelpItem) => (
|
renderItem={(item: HelpItem) => (
|
||||||
@@ -67,9 +91,9 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pagination defaultCurrent={page} total={50} pageSize={10} onChange={page => setPage(page)} />
|
<Pagination className="paging" defaultCurrent={page} total={50} pageSize={10} onChange={page => setPage(page)} />
|
||||||
|
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import {HelpItem} from "@/typings";
|
import { HelpItem } from "@/typings";
|
||||||
import {Descriptions} from "antd";
|
import { Descriptions } from "antd";
|
||||||
import {formatNumber, showUnixTime} from "@/lib/utils";
|
import { formatNumber, showUnixTime } from "@/lib/utils";
|
||||||
import PaymentStatus from "@/components/display/PaymentStatus";
|
import PaymentStatus from "@/components/display/PaymentStatus";
|
||||||
import ShippingStatus from "@/components/display/ShippingStatus";
|
import ShippingStatus from "@/components/display/ShippingStatus";
|
||||||
import OrderStatus from "@/components/display/OrderStatus";
|
import OrderStatus from "@/components/display/OrderStatus";
|
||||||
import Comment from "@/components/Comment";
|
import Comment from "@/components/Comment";
|
||||||
|
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
const ProductDetailComponent = ({id, openHome}: {id?: string|number, openHome: () => void}) => {
|
|
||||||
|
const ProductDetailComponent = ({ id, openHome }: { id?: string | number, openHome: () => void }) => {
|
||||||
|
|
||||||
const [product_info, setProduct] = useState<HelpItem>({});
|
const [product_info, setProduct] = useState<HelpItem>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _getInfo = async () => {
|
const _getInfo = async () => {
|
||||||
let result = await api.get('product/info?id=', {id});
|
let result = await api.get('product/info?id=', { id });
|
||||||
if(result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
setProduct(result.data);
|
setProduct(result.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -24,8 +26,11 @@ const ProductDetailComponent = ({id, openHome}: {id?: string|number, openHome: (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div><span className={'a-link'} onClick={openHome}>Back to home</span></div>
|
<div className="product-detail-header">
|
||||||
|
<div className="product-detail-header-left"><span className={'a-link'} onClick={openHome}><ArrowLeftOutlined /> Quay lại</span></div>
|
||||||
|
<div className="product-detail-header-right"><span className={'a-link'} onClick={() => alert('go to web')}>Xem sản phẩm tại web <ArrowRightOutlined /></span></div>
|
||||||
|
</div>
|
||||||
|
<div className="product-detail-content">
|
||||||
<Descriptions
|
<Descriptions
|
||||||
bordered
|
bordered
|
||||||
title=" "
|
title=" "
|
||||||
@@ -42,11 +47,13 @@ const ProductDetailComponent = ({id, openHome}: {id?: string|number, openHome: (
|
|||||||
{product_info.summary}
|
{product_info.summary}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="Hình ảnh">
|
<Descriptions.Item label="Hình ảnh">
|
||||||
<img src={product_info.img} style={{maxWidth: 150, maxHeight: 150}} alt={''} />
|
<img src={product_info.img} style={{ maxWidth: 150, maxHeight: 150 }} alt={''} />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
<div className="product-detail-comment">
|
||||||
<Comment item_type={'product'} item_id={product_info.id} />
|
<Comment item_type={'product'} item_id={product_info.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import {Avatar, List, Pagination} from "antd";
|
import { Avatar, List, Pagination, Select } from "antd";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import {HelpItem} from "@/typings";
|
import { HelpItem } from "@/typings";
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
|
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import {formatNumber} from "@/lib/utils";
|
import { formatNumber } from "@/lib/utils";
|
||||||
|
import { CheckOutlined, CloseOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
|
||||||
const ProductDescription = ({item}: {item: HelpItem}) => {
|
const ProductDescription = ({ item }: { item: HelpItem }) => {
|
||||||
const stock = item.in_stock ? 'Còn hàng' : 'Hết hàng';
|
const stock = item.in_stock ? <span className="pQuantity pQuanCOn"><CheckOutlined />Còn hàng</span> : <span className="pQuantity pQuanHet"><CloseOutlined />Hết hàng</span>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{formatNumber(item.price)} - {stock}
|
<span className="pPrice">{formatNumber(item.price)}</span>{stock}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: string|number) => void }) => {
|
const TopListComponent = (props: { defaultList: HelpItem[], openItem: (id?: string | number) => void }) => {
|
||||||
|
|
||||||
const {defaultList, openItem} = props;
|
const { defaultList, openItem } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List
|
<List
|
||||||
|
className="product-list"
|
||||||
itemLayout="horizontal"
|
itemLayout="horizontal"
|
||||||
dataSource={defaultList}
|
dataSource={defaultList}
|
||||||
renderItem={(item: HelpItem) => (
|
renderItem={(item: HelpItem) => (
|
||||||
@@ -41,11 +43,37 @@ const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
function handleChange(value: string) {
|
||||||
|
console.log(`selected ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
const FullListComponent = (props: {openItem: (id?: string|number) => void, params?: {[key: string]: any} }) => {
|
const SelectBoxCategory = () => {
|
||||||
|
return (
|
||||||
|
<div className="selectCategory">
|
||||||
|
<Select defaultValue="Danh mục" style={{ width: 120 }} onChange={handleChange}>
|
||||||
|
<Option value="1">Laptop</Option>
|
||||||
|
<Option value="2">Máy tính để bàn</Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const {openItem, params} = props;
|
const SelectBrandFilter = () => {
|
||||||
const item_list = useRef<HelpItem[]>( []);
|
return (
|
||||||
|
<div className="brandFilter">
|
||||||
|
<Select defaultValue="Thương Hiệu" style={{ width: 120 }} onChange={handleChange}>
|
||||||
|
<Option value="1">Dell</Option>
|
||||||
|
<Option value="2">HP</Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullListComponent = (props: { openItem: (id?: string | number) => void, params?: { [key: string]: any } }) => {
|
||||||
|
|
||||||
|
const { openItem, params } = props;
|
||||||
|
const item_list = useRef<HelpItem[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
|
|
||||||
@@ -55,22 +83,30 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let result = await api.get('product/list', params ? {...params, page} : {page});
|
let result = await api.get('product/list', params ? { ...params, page } : { page });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
item_list.current = result.data.list;
|
item_list.current = result.data.list;
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}) ();
|
})();
|
||||||
}, [page, params]);
|
}, [page, params]);
|
||||||
|
|
||||||
if(loading) {
|
if (loading) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="help-full-content">
|
||||||
|
<div className="product-list-head">
|
||||||
|
<div className="product-list-head-Left"><span className={'a-link'} onClick={() => alert('về trang chủ')}><ArrowLeftOutlined />Quay lại</span></div>
|
||||||
|
<div className="product-list-head-right">
|
||||||
|
<SelectBoxCategory />
|
||||||
|
<SelectBrandFilter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<List
|
<List
|
||||||
|
className="help-list-full product-list"
|
||||||
itemLayout="horizontal"
|
itemLayout="horizontal"
|
||||||
dataSource={item_list.current}
|
dataSource={item_list.current}
|
||||||
renderItem={(item: HelpItem) => (
|
renderItem={(item: HelpItem) => (
|
||||||
@@ -78,28 +114,28 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
|
|||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={<Avatar src={item.img} />}
|
avatar={<Avatar src={item.img} />}
|
||||||
title={<span className={'a-link'} >{item.name}</span>}
|
title={<span className={'a-link'} >{item.name}</span>}
|
||||||
description={<ProductDescription item={item}/>}
|
description={<ProductDescription item={item} />}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pagination defaultCurrent={page} total={50} pageSize={10} onChange={page => setPage(page)} />
|
<Pagination className="paging" defaultCurrent={page} total={50} pageSize={10} onChange={page => setPage(page)} />
|
||||||
|
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ProductListComponent = (props: {defaultList?: HelpItem[], openItem: (id?: string|number) => void , params?: {[key: string]: any} }) => {
|
const ProductListComponent = (props: { defaultList?: HelpItem[], openItem: (id?: string | number) => void, params?: { [key: string]: any } }) => {
|
||||||
|
|
||||||
const {defaultList, openItem, params} = props;
|
const { defaultList, openItem, params } = props;
|
||||||
|
|
||||||
if(defaultList) {
|
if (defaultList) {
|
||||||
return <TopListComponent defaultList={defaultList} openItem={openItem} />
|
return <TopListComponent defaultList={defaultList} openItem={openItem} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FullListComponent params={params} openItem={openItem}/>
|
return <FullListComponent params={params} openItem={openItem} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {Input} from "antd";
|
import {Input} from "antd";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {HelpType} from "@/typings";
|
import {HelpType} from "@/typings";
|
||||||
|
import { SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
@@ -18,11 +19,10 @@ const SearchBox = (props: {openComponent: (type: HelpType, params?: { [key: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="search-ct-right">
|
||||||
<Search
|
<Search
|
||||||
placeholder="Tìm kiếm sản phẩm hoặc kiến thức"
|
placeholder="Tìm kiếm sản phẩm hoặc kiến thức"
|
||||||
allowClear
|
allowClear
|
||||||
enterButton="Tìm"
|
|
||||||
size="middle"
|
size="middle"
|
||||||
onSearch={startSearch}
|
onSearch={startSearch}
|
||||||
value={keyword}
|
value={keyword}
|
||||||
|
|||||||
@@ -9,12 +9,34 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Krub', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-header {
|
.ant-layout-header {
|
||||||
background: black;
|
background: #001B34;
|
||||||
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-sider {
|
.ant-layout-sider {
|
||||||
@@ -31,7 +53,663 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* use for SPAN in place of A tag */
|
/* use for SPAN in place of A tag */
|
||||||
|
.box-chatboard {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.a-link {
|
.a-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #007aff;
|
color: #007aff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-chatboard {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100vh - 55px);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-chatboard-row {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLeft {
|
||||||
|
float: left;
|
||||||
|
background: #002344;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLeft ::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLeft ::-webkit-scrollbar-track {
|
||||||
|
background: #002344;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLeft ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenter {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
padding: 20px;
|
||||||
|
background: #F1EFEF;
|
||||||
|
padding-top: 10px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentRight {
|
||||||
|
width: 100%;
|
||||||
|
float: right;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-chatboard-col {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterHeader {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.contentCenterHeaderAdmin {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.contentCenterHeaderAdmin span.h-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #848484;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-right: 10px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.contentCenterHeaderAdmin span.h-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #848484;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.cuscontentCenterHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .avatar-cus {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .avatar-cus img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
float: left;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .ant-badge-status {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: -7px;
|
||||||
|
right: 0px;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .ant-badge-status-text {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .avatar-cus .ant-badge-status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: solid 2px #001F3D;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #9A9A9A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .avatar-cus .ant-badge-status-dot.ant-badge-status-green {
|
||||||
|
background: #37EE14;
|
||||||
|
}
|
||||||
|
.cuscontentCenterHeader .info-cus {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .info-cus .name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuscontentCenterHeader .info-cus .address {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.contentCenterCt {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 53px);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.contentCenterCt .ant-row {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.contentCenterCt .ant-row .ant-col {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.contentCenterCtColLeft {
|
||||||
|
border-right: solid 1px #F1EFEF;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRightTitle {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0px 15px;
|
||||||
|
background: #F1EFEF;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #001529;
|
||||||
|
border-left: solid 7px #5DD498;
|
||||||
|
}
|
||||||
|
.tab-content-mes {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-list {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-list .ant-tabs-tab {
|
||||||
|
line-height: 30px;
|
||||||
|
border: solid 1px #D9D9D9;
|
||||||
|
padding: 0px 10px !important;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
margin-right: 5px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-list .ant-tabs-tab.ant-tabs-tab-active {
|
||||||
|
border-color: #1890FF !important;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-list .ant-tabs-tab .ant-tabs-tab-btn {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #001529;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-list .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
|
color: #1890FF;
|
||||||
|
}
|
||||||
|
.ant-tabs-top > .ant-tabs-nav::before, .ant-tabs-bottom > .ant-tabs-nav::before, .ant-tabs-top > div > .ant-tabs-nav::before, .ant-tabs-bottom > div > .ant-tabs-nav::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-operations .ant-tabs-nav-more {
|
||||||
|
padding: 0px 16px !important;
|
||||||
|
line-height: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: #1890FF;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-nav-operations .ant-tabs-nav-more::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRightItem {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(50% - 10px);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-content-holder {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 102px);
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-content {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.contentCenterCtColRight .ant-tabs-content::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterCtColRight .ant-tabs-content::-webkit-scrollbar-track {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentCenterCtColRight .ant-tabs-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #C4C4C4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.taoghichu-input {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
border: solid 1px #707070;
|
||||||
|
height: 75px !important;
|
||||||
|
border-radius: 5px !important;
|
||||||
|
}
|
||||||
|
.taoghichu-submit {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.taoghichu-submit .taoghichu-bt-update {
|
||||||
|
padding: 0px 15px;
|
||||||
|
line-height: 32px;
|
||||||
|
border: solid 1px #D9D9D9;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.taoghichu-submit .taoghichu-bt-send {
|
||||||
|
width: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
padding: 0px;
|
||||||
|
border: none;
|
||||||
|
background: #1890FF;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.ant-form-item {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-label {
|
||||||
|
width: 120px !important;
|
||||||
|
max-width: 120px !important;
|
||||||
|
flex: 0 0 120px;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-label label {
|
||||||
|
float: left;
|
||||||
|
white-space: break-spaces;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-control {
|
||||||
|
width: calc(100% - 120px) !important;
|
||||||
|
max-width: calc(100% - 120px) !important;
|
||||||
|
flex: 0 0 calc(100% - 120px);
|
||||||
|
}
|
||||||
|
.formTaoGhiChu .ant-form-item .ant-form-item-control {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-control input {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0px 10px;
|
||||||
|
border: solid 1px #D9D9D9;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-control .ant-space {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ant-form-item .ant-form-item-control .ant-checkbox-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.formTaoHoTro .ant-form-item:nth-child(6) .ant-form-item-label {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
.formTaoHoTro .ant-form-item:nth-child(6) .ant-form-item-control {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
.ant-select-show-arrow {
|
||||||
|
min-width: 150px;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.formTaoDonHang .ant-form-item:nth-child(2) .ant-select {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.formTaoDonHang .ant-form-item:nth-child(5) .ant-select {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.formTaoDonHang .ant-form-item .ant-form-item {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.formThongTin .ant-form-item:nth-child(3) .ant-select-selector {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.formThongTin .ant-form-item:nth-child(9) .ant-select-selector {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.contentRightHeader {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.contentRightHeader .anticon-close {
|
||||||
|
float: right;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.search-ct-right {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.contentRight .scrollable {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.helpProduct {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.titleHelpRight {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.titleHelpRight span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #5DD498;
|
||||||
|
}
|
||||||
|
.helpContent {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 108px);
|
||||||
|
}
|
||||||
|
.helpContent .helpContentRight {
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.helpContentRightScroll {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 41px);
|
||||||
|
}
|
||||||
|
.helpContentRightScroll::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentRightScroll::-webkit-scrollbar-track {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentRightScroll::-webkit-scrollbar-thumb {
|
||||||
|
background: #C4C4C4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.product-list li {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.product-list li .ant-avatar-image {
|
||||||
|
width: 85px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-top: 85px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.product-list li .ant-avatar-image img {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.product-list li .ant-list-item-meta-avatar {
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
.product-list li .ant-list-item-meta-content {
|
||||||
|
width: calc(100% - 95px);
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.product-list li .ant-list-item-meta-content .ant-list-item-meta-title {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.product-list li .ant-list-item-meta-content .ant-list-item-meta-title span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.ant-list-item-meta-description {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.ant-list-item-meta-description .pPrice {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #EC1010;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.pQuantity {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.pQuantity.pQuanCOn {
|
||||||
|
color: #5DD498;
|
||||||
|
}
|
||||||
|
.pQuantity.pQuanHet {
|
||||||
|
color: #EC1010;
|
||||||
|
}
|
||||||
|
.Article-list li {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.Article-list li h4 span {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6D6D6D;
|
||||||
|
}
|
||||||
|
.Article-list li .ant-list-item-meta-description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6D6D6D;
|
||||||
|
}
|
||||||
|
.help-full-content {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 130px);
|
||||||
|
}
|
||||||
|
.help-full-content .help-list-full {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 45px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.help-full-content .help-list-full::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-full-content .help-list-full::-webkit-scrollbar-track {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-full-content .help-list-full::-webkit-scrollbar-thumb {
|
||||||
|
background: #C4C4C4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.paging {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.paging li {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
.product-list-head {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.product-list-head-Left span {
|
||||||
|
color: #37EE14;
|
||||||
|
}
|
||||||
|
.product-list-head-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.product-list-head-right .ant-select {
|
||||||
|
width: 120px !important;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
min-width: 100px !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.product-list-head-right .selectCategory {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.product-detail-header {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.product-detail-header-left span {
|
||||||
|
color: #37EE14;
|
||||||
|
}
|
||||||
|
.product-detail-header-right span {
|
||||||
|
color: #1890FF;
|
||||||
|
}
|
||||||
|
.product-detail-content {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
height: calc(100% - 120px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.product-detail-content::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
.product-detail-content::-webkit-scrollbar-track {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.product-detail-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #C4C4C4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.product-detail-content table {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.product-detail-comment {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.form-commet-all {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.form-commet-all h3 {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.form-commet-all #complex-form {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-commet-all #complex-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
border: solid 1px #707070;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.form-commet-all .ant-form-item .ant-form-item-control {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
.form-commet-all .ant-comment {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.article-detail-content-ct {
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6D6D6D;
|
||||||
|
}
|
||||||
|
.fix-ab-ct-right {
|
||||||
|
width: 470px;
|
||||||
|
float: left;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.fix-ab-ct-right .contentRightHeader .anticon-close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.content-chatboard-col-big .contentCenterHeaderAdmin {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 1366px) {
|
||||||
|
.contentCenterCtColRight .ant-tabs-content {
|
||||||
|
max-height: 140px;
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user