giao dien moi

This commit is contained in:
2021-05-22 09:54:28 +07:00
parent ab2830f449
commit 4770ce26d2
25 changed files with 23237 additions and 416 deletions

21576
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,10 @@
"@types/node": "^12.20.7",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"http-proxy-middleware": "^0.19.1",
"react-app-rewired": "^2.1.8",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.1",
"http-proxy-middleware": "^0.19.1"
"web-vitals": "^1.1.1"
},
"scripts": {
"start": "react-app-rewired start",

View File

@@ -1,30 +1,30 @@
import React, {createRef, useState, Fragment, FC} from 'react';
import {Layout, Col, Row, Button, Image} from 'antd';
import { EditOutlined, CloseOutlined } from '@ant-design/icons';
import React, { createRef, useState, Fragment, FC } from 'react';
import { Layout, Col, Row, Button, Image } from 'antd';
import { EditOutlined, CloseOutlined, AppstoreOutlined } from '@ant-design/icons';
import ConversationList from "@/components/ConversationList";
import Chatbox from "@/components/Chatbox";
import CustomerInfo from "@/components/CustomerInfo";
import ActionTabs from "@/components/ActionTabs";
import {HelpSideBar} from "@/components/Help";
import { HelpSideBar } from "@/components/Help";
import HeaderComponent from "@/components/HeaderComponent";
import {useDispatch, useSelector} from "react-redux";
import {AppState} from "@/store/typing";
import {UserInfo} from "@/typings/user";
import {actions} from "@/store/actions";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "@/store/typing";
import { UserInfo } from "@/typings/user";
import { actions } from "@/store/actions";
import GlobalModal from '@/components/GlobalModal';
import GlobalDrawer from "@/components/GlobalDrawer";
import BadgeStatus from "@/components/BadgeStatus";
import NetworkError from "@/components/Error/NetworkError";
import {ClientSettings} from "@/typings";
import { ClientSettings } from "@/typings";
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
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';
@@ -33,11 +33,11 @@ const { Header, Content, Sider } = Layout;
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 dispatch = useDispatch();
if( ! user ) {
if (!user) {
return null;
}
@@ -50,17 +50,24 @@ const UserNameForm = ({user}: {user: UserInfo | null}) => {
}));
setFormOpen(false);
}
if(!openForm) {
const avatar = user.avatar || DEFAULT_AVATAR;
if (!openForm) {
return (
<Fragment>
<BadgeStatus online={user.online || false}>
{user.name}
{
user.typing && <Image preview={false} style={{height:20}} src={TYPING_ANIMATION_IMAGE} />
}
</BadgeStatus> ( <EditOutlined onClick={() => setFormOpen(true)} title={'Thay đổi tên'} /> )
</Fragment>
<div className="cuscontentCenterHeader">
<div className="avatar-cus">
<img className="conversation-photo" src={avatar} alt="conversation" />
<BadgeStatus online={user.online || false}>
</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} />
}<div className="cuscontentCenterHeaderEdit">( <EditOutlined onClick={() => setFormOpen(true)} title={'Thay đổi tên'} /> )</div>
</div>
)
}
@@ -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);
return filtered_list.length > 0 ? filtered_list[0] : null;
}
const ShowUserSpace = () => {
const ShowUserSpace = ({openHelp}: {openHelp: () => any}) => {
// 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();
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
dispatch(actions.chatWithUser(user_list[0].id));
}
// else show the dashboard
if( ! chatting_user_info ) {
if (!chatting_user_info) {
return <DashBoard />
}
return (
<Fragment>
<div>
<UserNameForm user={chatting_user_info} />
<Fragment>
<div className="contentCenterHeader">
<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 className="contentCenterCt">
<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*/}
<Chatbox key={chatting_user_info.id} user_info={chatting_user_info} />
</Col>
<Col span={12} className="scrollable" style={{height: 600, overflow: 'auto'}}>
<h3 style={{fontSize:16, fontWeight:'bold'}}>Lựa chọn</h3>
<Col span={12} className="contentCenterCtColRight">
<div className="contentCenterCtColRightItem">
<h3 className="contentCenterCtColRightTitle" style={{ fontSize: 16, fontWeight: 'bold' }}>Lựa chọn</h3>
<ActionTabs customer_id={chatting_user_info.id} />
<h3 style={{fontSize:16, fontWeight:'bold'}}>Thông tin khách hàng</h3>
<CustomerInfo user_info={chatting_user_info} />
<ActionTabs customer_id={chatting_user_info.id} />
</div>
<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} />
</div>
</Col>
</Row>
</Fragment>
</div>
</Fragment>
)
}
const App: FC<{client_setting: ClientSettings}> = ({client_setting}) => {
const App: FC<{ client_setting: ClientSettings }> = ({ client_setting }) => {
const HEADER_HEIGHT = 70;
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 (
<Layout>
<Header style={{ position: 'fixed', zIndex: 1, width: '100%' }}>
<Layout className={'box-chatboard'}>
<Header style={{ width: '100%' }}>
<HeaderComponent />
</Header>
<Layout style={LAYOUT_CLOSED}>
<Layout className={'content-chatboard'}>
<Row className={'content-chatboard-row'}>
<Col className={'content-chatboard-col'} span={4}>
<div className="contentLeft">
<ConversationList />
</div>
</Col>
{
windowWidth <= 1366 ?
<Col className={'content-chatboard-col'} span={20}>
<Content className="contentCenter">
<ShowUserSpace openHelp={() => setHelpClose(true)} />
</Content>
</Col>
:
<Col className={'content-chatboard-col content-chatboard-col-big'} span={14}>
<Content className="contentCenter">
<ShowUserSpace openHelp={() => setHelpClose(true)} />
</Content>
</Col>
}
<Sider width={250} className="scrollable">
<ConversationList />
</Sider>
<Content>
<ShowUserSpace />
</Content>
{
!closeHelp &&
<Sider width={400} >
<h2>Trợ giúp <CloseOutlined onClick={() => setHelpClose(true)} title={'Đóng'} /> </h2>
<HelpSideBar />
</Sider>
}
{
windowWidth <= 1366 ?
(
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 />
</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>
<NetworkError />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -54,8 +54,13 @@ const NoteForm = ({customer_id}: {customer_id: string|number}) => {
e.preventDefault();
form.submit();
}}
className="taoghichu-input"
/>
</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>
)
}

View File

@@ -12,27 +12,27 @@ const { TabPane } = Tabs;
const ActionTabs = ({customer_id}: {customer_id: string|number}) => {
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 />} >
<CreateNoteComponent customer_id={customer_id}/>
</Suspense>
</TabPane>
<TabPane tab="Tạo hỗ trợ" key="ticket">
<TabPane className="formTaoHoTro" tab="Tạo hỗ trợ" key="ticket">
<Suspense fallback={<Loading />} >
<CreateSupportComponent customer_id={customer_id}/>
</Suspense>
</TabPane>
<TabPane tab="Tạo đơn hàng" key="order">
<TabPane className="formTaoDonHang" tab="Tạo đơn hàng" key="order">
<Suspense fallback={<Loading />} >
<CreateOrderComponent customer_id={customer_id}/>
</Suspense>
</TabPane>
<TabPane tab="Tag" key="tag">
<TabPane className="formTag" tab="Tag" key="tag">
<Suspense fallback={<Loading />} >
<CreateTag customer_id={customer_id}/>
</Suspense>

View File

@@ -39,7 +39,15 @@
}
.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 {
@@ -106,3 +114,123 @@
border-top-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;
}

View File

@@ -1,20 +1,20 @@
import React, {Component, createRef, Fragment} from "react";
import {connect} from "react-redux";
import {Dispatch} from "redux";
import React, { Component, createRef, Fragment } from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import debounce from "lodash/debounce";
import {UserInfo} from "@/typings/user";
import {ChatboxTextMessage} from "@/typings/message.d";
import {sendTextMessageToServer} from "@/lib/messaging";
import {getCurrentUTCTimestamp} from "@/lib/utils";
import {getUserChatHistory} from "@/lib/api";
import {AppState, NetworkingStatusType} from "@/store/typing";
import {actions} from "@/store/actions";
import {NOTIFICATIONS} from "@/constant/text";
import {getAdminInfo} from "@/lib/user";
import storage, {userChatHistoryStorageKey} from "@/lib/storage";
import { UserInfo } from "@/typings/user";
import { ChatboxTextMessage } from "@/typings/message.d";
import { sendTextMessageToServer } from "@/lib/messaging";
import { getCurrentUTCTimestamp } from "@/lib/utils";
import { getUserChatHistory } from "@/lib/api";
import { AppState, NetworkingStatusType } from "@/store/typing";
import { actions } from "@/store/actions";
import { NOTIFICATIONS } from "@/constant/text";
import { getAdminInfo } from "@/lib/user";
import storage, { userChatHistoryStorageKey } from "@/lib/storage";
import {MessageItem} from "./MessageItem";
import { MessageItem } from "./MessageItem";
import InputMessage from "./InputMessage";
import TypingNotification from "./TypingNotification";
@@ -93,7 +93,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
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();
}
}
@@ -103,28 +103,28 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
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
// 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
const stored_messages: ChatboxTextMessage[] = await storage.get(userChatHistoryStorageKey(user_info.id)) || [];
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;
}
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;
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) {
console.log('old_messages: this.noMoreHistoryMessage = true')
@@ -133,13 +133,13 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
}
console.log('dispatching historied messages');
dispatch(actions.addHistoryMessage({[user_info.id]: old_messages}));
dispatch(actions.addHistoryMessage({ [user_info.id]: old_messages }));
}
}
scrollToBottom = () => {
// no scroll if user view history
if ( this.disableScrollBottom ) {
if (this.disableScrollBottom) {
return;
}
@@ -148,7 +148,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
showNotification = () => {
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[] = [];
@@ -162,9 +162,9 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
list_messages.push(NOTIFICATIONS['user_offline']);
}
if(list_messages.length > 0) {
if (list_messages.length > 0) {
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))
}
@@ -181,18 +181,18 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
console.log('start handleChatboxScroll at ' + new Date().getSeconds());
//console.log(event);
if(!event.target) {
if (!event.target) {
console.log('handleChatboxScroll: event.currentTarget');
return;
}
if(this.noMoreHistoryMessage) {
if (this.noMoreHistoryMessage) {
console.log('handleChatboxScroll: noMoreHistoryMessage');
return;
}
// 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;
}
@@ -204,7 +204,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
this.loadingHistoryStatus = 'loading';
this.disableScrollBottom = true;
console.log('handleChatboxScroll should get the history now ... ');
this.getChatHistory(true).then();
this.getChatHistory(true).then();
}
}, 300)
@@ -214,18 +214,18 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
const { user_info, dispatch, network_connection } = this.props; // , admin_info, user_info
//const {typed_message} = this.state;
if(typed_message === '') {
if (typed_message === '') {
return;
}
// reset when user send new message
if(this.disableScrollBottom) {
if (this.disableScrollBottom) {
this.disableScrollBottom = false;
}
// cannot send message when network_connection is offline
// and restore message to the input
if(network_connection === 'offline') {
if (network_connection === 'offline') {
console.log("network_connection =offline");
return;
}
@@ -241,7 +241,7 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
deliveryStatus: 0,
} 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
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 = () => {
const {loadingHistory} = this.state;
const { loadingHistory } = this.state;
if(loadingHistory) {
if (loadingHistory) {
return (
<div style={{fontStyle:'italic', fontSize:11, textAlign: 'center'}} >Loading ...</div>
<div style={{ fontStyle: 'italic', fontSize: 11, textAlign: 'center' }} >Loading ...</div>
)
}
@@ -264,36 +264,49 @@ class Chatbox extends Component<ChatboxProps, ChatboxState> {
render() {
const { network_connection, user_info, chat_messages} = this.props;
const { network_connection, user_info, chat_messages } = this.props;
const admin_info = getAdminInfo();
return (
<Fragment >
<div
style={{height: 600, overflow: 'auto'}}
onScroll={this.handleChatboxScroll}
// onClick={listenForInChatAction}
ref={this.scrollToHistoryDiv}
className={'message-list'}
>
<div className={'message-list-all'}>
<div
style={{ overflow: 'auto' }}
onScroll={this.handleChatboxScroll}
// onClick={listenForInChatAction}
ref={this.scrollToHistoryDiv}
className={'message-list'}
>
<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>
}
<MessageItem key={message.time + '-' + index} is_me={admin_info.id === message.from} {...message} />
</>
)
})
}
{user_info.typing && <TypingNotification />}
</div>
{this.showNotification()}
<div ref={this.scrollBottomDiv} />
{ user_info.typing && <TypingNotification /> }
</div>
{ this.showNotification() }
<div ref={this.scrollBottomDiv} />
</div>
<InputMessage
sendMessage={this.sendMessage}
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,
chat_messages: state.current_messages[ownProps.user_info.id] || [],
});

View File

@@ -62,7 +62,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
return (
<Fragment>
<div className="chatBottomContent">
<Input.TextArea
// ref={this.inputRef}
value={typed_message}
@@ -100,7 +100,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
okText="Yes"
cancelText="No"
>
<span style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Forbid</span>
<span className="bt-mes-bt" style={{cursor: 'pointer'}} ><MinusCircleOutlined /> Forbid</span>
</Popconfirm>
@@ -111,7 +111,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
okText="Yes"
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>
&nbsp;
@@ -121,7 +121,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
content={<PickAdminOnlineToTransfer />}
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>
@@ -130,7 +130,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
ref={imageUploadWithPreviewRef}
auth={admin_info.jwt||''}
// 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'}
showUploadList={false}
multiple={false}
@@ -151,7 +151,7 @@ const InputMessage = ({network_connection, sendMessage, user_info} : {network_co
}
}}
/>
</Fragment>
</div>
)
};

View File

@@ -1,14 +1,15 @@
import {isUrlImage, validURL} from "@/lib/validation";
import {maskExternalUrl, randomBetween, showUnixTime} from "@/lib/utils";
import {ChatboxTextMessage} from "@/typings/message.d";
import { isUrlImage, validURL } from "@/lib/validation";
import { maskExternalUrl, randomBetween, showUnixTime } from "@/lib/utils";
import { ChatboxTextMessage } from "@/typings/message.d";
import React from "react";
import { DEFAULT_AVATAR } from "@/config";
export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
let {deliveryStatus, from, content, time, is_me } = props;
export const MessageItem = (props: { is_me: boolean } & ChatboxTextMessage) => {
let { deliveryStatus, from, content, time, is_me } = props;
let time_in_second = Math.round(time / 1000);
if( ! content ) return null;
if (!content) return null;
let image = (isUrlImage(content)) ? content : '';
let status_colors = {
@@ -18,24 +19,24 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
};
let html;
if ( is_me ) from = 'me';
if (is_me) from = 'me';
if (from === 'bot') {
return (
<div>
<span dangerouslySetInnerHTML={{__html: content}} />
<span dangerouslySetInnerHTML={{ __html: content }} />
</div>
)
}
if (image !== '') {
if(_isImageUploadedToOurServer(image)) {
if (_isImageUploadedToOurServer(image)) {
html = `<a href="${maskExternalUrl(image)}" title="Click để xem ảnh" target="_blank"><img src={image} alt={""}/></a>`
return (
<div>
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
<span dangerouslySetInnerHTML={{__html: html}} />
<span dangerouslySetInnerHTML={{ __html: html }} />
</div>
)
}
@@ -44,19 +45,19 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
return (
<div>
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
<span dangerouslySetInnerHTML={{__html: html}} />
<div style={{color: 'red'}}>(nh chưa kiểm đnh. Cần thận trọng khi xem)</div>
<span dangerouslySetInnerHTML={{ __html: html }} />
<div style={{ color: 'red' }}>(nh chưa kiểm đnh. Cần thận trọng khi xem)</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>`
return (
<div>
<b>{from}</b><i>{showUnixTime(time_in_second)}</i>
<span dangerouslySetInnerHTML={{__html: html}} />
<div style={{color: 'red'}}>(Link chưa kiểm đnh. Cần thận trọng khi xem)</div>
<span dangerouslySetInnerHTML={{ __html: html }} />
<div style={{ color: 'red' }}>(Link chưa kiểm đnh. Cần thận trọng khi xem)</div>
</div>
)
}
@@ -65,6 +66,7 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
// todo: remove this
let random_number = randomBetween(1, 100);
let message_from_me = random_number % 2 === 0;
const avatar = DEFAULT_AVATAR;
return (
@@ -77,20 +79,27 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
//`${endsSequence ? 'end' : ''}`
].join(' ')}>
<div className="timestamp">
{from}: { showUnixTime(time_in_second) }
</div>
<div className="bubble-container">
<div className="bubble" style={{color: (deliveryStatus !== 4) ? status_colors['sending'] : ''}} >
{ content }
<div className="mes-content">
<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 className="bubble-container">
<div className="bubble" style={{ color: (deliveryStatus !== 4) ? status_colors['sending'] : '' }} >
{content}
</div>
</div>
{
deliveryStatus === 5 && (<div style={{ color: 'red' }}>(Lỗi xảy ra, tin nhắn chưa đưc gửi)</div>)
}
</div>
</div>
{
deliveryStatus === 5 && (<div style={{color: 'red'}}>(Lỗi xảy ra, tin nhắn chưa đưc gửi)</div>)
}
<div>deliveryStatus :{deliveryStatus} | sq:{props.sequence}</div>
</div>
)
@@ -98,7 +107,7 @@ export const MessageItem = (props: {is_me: boolean} & ChatboxTextMessage) => {
// 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
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;

View File

@@ -104,7 +104,7 @@ const CommentList = ({item_type, item_id} : {item_type: string, item_id?: string
return (
<div >
<div className="form-commet-all">
<Divider />

View File

@@ -1,63 +1,265 @@
.conversation-list {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
height: 100%;
}
.conversation-search {
padding: 10px;
display: flex;
flex-direction: column;
padding: 10px;
display: flex;
flex-direction: column;
}
.conversation-search-input {
background: #f4f4f8;
padding: 8px 10px;
border-radius: 10px;
border: none;
font-size: 14px;
border: none !important;
font-size: 14px !important;
padding: 0px !important;
}
.conversation-search-input::placeholder {
text-align: center;
text-align: center;
}
.conversation-search-input:focus::placeholder {
text-align: left;
text-align: left;
}
.conversation-list-item {
display: flex;
align-items: center;
padding: 10px;
display: flex;
align-items: center;
padding: 10px;
}
.conversation-list-item-selected {
background: #ffcc00;
background: #ffcc00;
}
.conversation-list-item:hover {
background: #ffcc00;
cursor: pointer;
background: #ffcc00;
cursor: pointer;
}
.conversation-photo {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px;
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px;
}
.conversation-title {
font-size: 14px;
font-weight: bold;
text-transform: capitalize;
margin: 0;
font-size: 14px;
font-weight: bold;
text-transform: capitalize;
margin: 0;
}
.conversation-snippet {
font-size: 14px;
color: #CCCCCC;
margin: 0;
font-size: 14px;
color: #CCCCCC;
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;
}

View File

@@ -1,13 +1,13 @@
import React, {createRef, useState} from 'react';
import React, { createRef, useState } from 'react';
import debounce from "lodash/debounce";
import {Image, Input} from "antd";
import { Image, Input, Menu, Dropdown, Layout } from "antd";
import classNames from "classnames";
import {useDispatch, useSelector} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import {AppState} from "@/store/typing";
import {UserInfo} from "@/typings/user";
import {actions} from "@/store/actions";
import {DEFAULT_AVATAR} from "@/config";
import { AppState } from "@/store/typing";
import { UserInfo } from "@/typings/user";
import { actions } from "@/store/actions";
import { DEFAULT_AVATAR } from "@/config";
import BadgeStatus from "@/components/BadgeStatus";
import Toolbar from '../Toolbar';
@@ -15,13 +15,15 @@ import ToolbarButton from '../ToolbarButton';
import TYPING_ANIMATION_IMAGE from "@/assets/typing-animation.gif";
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!!
const ConversationSearch = ({setKeyword} : {setKeyword: (keyword: string) => void}) => {
const ConversationSearch = ({ setKeyword }: { setKeyword: (keyword: string) => void }) => {
const search_wait_time = 300; // milli seconds
const inputRef = createRef<Input>();
const startSearch = () => {
@@ -32,15 +34,24 @@ const ConversationSearch = ({setKeyword} : {setKeyword: (keyword: string) => voi
}
return (
<div className="conversation-search">
<Input
ref={inputRef}
type="search"
className="conversation-search-input"
placeholder="Tìm kiếm"
onChange={debounce(startSearch, search_wait_time)}
allowClear
/>
<div className="box-search-customer">
<div className="search-customer-form">
<Input
ref={inputRef}
type="search"
className="conversation-search-input search-input"
placeholder="Tìm kiếm"
onChange={debounce(startSearch, search_wait_time)}
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>
);
}
@@ -68,21 +79,86 @@ const ConversationListItem = (props: { user: UserInfo, unread: { total: number,
return (
<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" />
</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} />
}
<BadgeStatus online={user.online || false}>
</BadgeStatus>
</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>
);
}
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="/">
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 = () => {
@@ -95,7 +171,7 @@ const ConversationList = () => {
total: 10,
messages: ['Vâng ạ', 'Xin chào']
},
"12312312334": {
"12312312334": {
total: 3,
messages: ['Vâng ạ', 'Xin chào']
},
@@ -103,40 +179,31 @@ const ConversationList = () => {
}
//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 = '') => {
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 (
<div className="conversation-list">
<div className="conversation-list">
<ConversationSearch setKeyword={setKeyword} />
<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" />
]}
/>
<FilterSortConversationList />
<ConversationSearch setKeyword={setKeyword} />
{
getUserList(keyword).map(
(user: UserInfo) => <ConversationListItem key={user.id} user={user} unread={un_read_message_per_user[user.id] || no_unread} />
)
}
</div>
<div className="list-customer-chat">
{
getUserList(keyword).map(
(user: UserInfo) => <ConversationListItem key={user.id} user={user} unread={un_read_message_per_user[user.id] || no_unread} />
)
}
</div>
</div>
);
}

View File

@@ -15,8 +15,8 @@ const BrowseHistoryComponent = React.lazy(() => import('./components/Brow
const CustomerInfo = ({user_info}: {user_info: UserInfo}) => {
return (
<Tabs type="card">
<TabPane tab="Thông tin" key="info">
<Tabs className="tab-content-mes" type="card">
<TabPane className="formThongTin" tab="Thông tin" key="info">
<Suspense fallback={<Loading />} >
<CustomerInfoComponent user_info={user_info} key={'customer-info'}/>
</Suspense>

View File

@@ -1,13 +1,14 @@
import React, {Fragment} from "react";
import {useDispatch, useSelector, shallowEqual} from "react-redux";
import {actions} from "@/store/actions";
import {AppState} from "@/store/typing";
import {getAdminInfo} from "@/lib/user";
import {Col, Row, Image, Menu, Dropdown} from "antd";
import { DownOutlined, TeamOutlined, UserOutlined, QuestionOutlined } from '@ant-design/icons';
import React, { Fragment } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";
import { actions } from "@/store/actions";
import { AppState } from "@/store/typing";
import { getAdminInfo } from "@/lib/user";
import { Col, Row, Image, Menu, Dropdown } from "antd";
import { DownOutlined, TeamOutlined, UserOutlined, CaretDownFilled, QuestionCircleOutlined } from '@ant-design/icons';
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 = (
@@ -34,7 +35,7 @@ const menu = (
const HeaderComponent = () => {
const admin_info = getAdminInfo();
const {stats, admin_list} = useSelector((state: AppState) => (
const { stats, admin_list } = useSelector((state: AppState) => (
{
stats: state.stats,
admin_list: state.admin_list,
@@ -43,17 +44,26 @@ const HeaderComponent = () => {
const current_stats: {
user_online: number,
} = (stats !== '') ? JSON.parse(stats) : { user_online: 0};
} = (stats !== '') ? JSON.parse(stats) : { user_online: 0 };
const dispatch = useDispatch();
const setOpenModal = (component: string) => {
dispatch(actions.openGlobalModal({component, args: {}}))
dispatch(actions.openGlobalModal({ component, args: {} }))
}
const handleMenuClick = (e: MenuInfo) => {
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 ADMIN_ONLINE = admin_list.filter(admin => admin.online).length;
@@ -64,17 +74,25 @@ const HeaderComponent = () => {
<Fragment>
<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 mode="horizontal" onClick={handleMenuClick}>
<Menu.Item key="user" icon={<UserOutlined />} >
<Menu className={'header-info'} mode="horizontal" onClick={handleMenuClick}>
<Menu.Item className={'header-info-click'} key="user" icon={<UserOutlined />} >
Người dùng online ({current_stats.user_online})
</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})
</Menu.Item>
<Menu.Item key="help" icon={<QuestionOutlined />}>
<Menu.Item className={'header-info-click'} key="help" icon={<QuestionCircleOutlined />}>
Trợ giúp
</Menu.Item>
</Menu>
@@ -82,18 +100,19 @@ const HeaderComponent = () => {
</Col>
<Col span={4}>
<Dropdown overlay={menu}>
<Fragment>
<Image
src={'https://chatngay-static.glee.vn/avatar/120_a1.jpg'}
style={{width: 50, height: 50}}
preview={false}
/>
<span className="name">{admin_info.name}</span>
</Fragment>
</Dropdown>
<div className={'header-avatar'}>
<Dropdown overlay={menuAdminAccount}>
<a className="ant-dropdown-link ant-dropdown-link-style-new" onClick={e => e.preventDefault()}>
<Image
src={'https://chatngay-static.glee.vn/avatar/120_a1.jpg'}
style={{ width: 50, height: 50 }}
preview={false}
className={'header-avatar-img'}
/>
<span className="name-admin">{admin_info.name} <CaretDownFilled /></span>
</a>
</Dropdown>
</div>
</Col>
</Row>

View File

@@ -1,9 +1,73 @@
.header {
color: white;
width: 100%;
height: 55px;
background: #001B34;
display: flex;
align-items: center;
}
.ant-layout-header .ant-menu{
background: black;
.ant-layout-header .ant-menu {
background: #001B34;
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;
}

View File

@@ -1,8 +1,9 @@
import React, {useEffect, useState, Suspense} from "react";
import React, { useEffect, useState, Suspense } from "react";
import api from "@/lib/api";
import Loading from "@/components/Loading";
import {HelpType, OpenHelpComponent} from "@/typings";
import { HelpType, OpenHelpComponent } from "@/typings";
import SearchBox from "./components/SearchBox";
import { ArrowRightOutlined } from '@ant-design/icons';
const ProductListComponent = React.lazy(() => import('./components/ProductList'));
const ArticleListComponent = React.lazy(() => import('./components/ArticleList'));
@@ -14,13 +15,13 @@ const WINDOW_HEIGHT = global.window.innerHeight;
const HELP_HEIGHT = WINDOW_HEIGHT - 150;
const HomeComponent = (props: {openComponent: (type: HelpType, params?: { [key: string] : any }) => void}) => {
const [help, setHelp] = useState<{product: any[], article: any[]}>({product:[], article:[]});
const HomeComponent = (props: { openComponent: (type: HelpType, params?: { [key: string]: any }) => void }) => {
const [help, setHelp] = useState<{ product: any[], article: any[] }>({ product: [], article: [] });
useEffect(() => {
const _getHelp = async () => {
let result = await api.get('help/home');
if(result.status === 'ok') {
if (result.status === 'ok') {
setHelp(result.data);
}
};
@@ -28,56 +29,58 @@ const HomeComponent = (props: {openComponent: (type: HelpType, params?: { [key:
}, []);
return (
<>
<div className={'scrollable'} style={{height: HELP_HEIGHT}}>
<h3 style={{fontSize: 16}}>Sản phẩm: <span className={'a-link'} onClick={() => props.openComponent('product-list')}>Xem het</span></h3>
<ProductListComponent
defaultList={help.product.slice(0, 5)}
openItem={(id) => props.openComponent('product-detail', {id})}
/>
<h3 style={{fontSize: 16}}>Kho kiến thức: <span className={'a-link'} onClick={() => props.openComponent('article-list')}>Xem het</span></h3>
<ArticleListComponent
defaultList={help.article.slice(0, 5)}
openItem={(id) => props.openComponent('article-detail', {id})}
/>
<div className="helpContent">
<div className={'helpContentRight'}>
<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
defaultList={help.product.slice(0, 5)}
openItem={(id) => props.openComponent('product-detail', { id })}
/>
</div>
</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
defaultList={help.article.slice(0, 5)}
openItem={(id) => props.openComponent('article-detail', { id })}
/>
</div>
</div>
</div>
)
}
const HelpSideBar = () => {
const [component, setComponent] = useState<OpenHelpComponent>({type: 'home'});
const [component, setComponent] = useState<OpenHelpComponent>({ type: 'home' });
const openComponent = (type: HelpType, params?: { [key: string] : any }) => {
setComponent({type, params});
const openComponent = (type: HelpType, params?: { [key: string]: any }) => {
setComponent({ type, params });
}
const getLoadedComponent = () => {
if(component.type === 'product-list') {
return <ProductListComponent params={component.params} openItem={(id) => openComponent('product-detail', {id})} />;
if (component.type === 'product-list') {
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')} />;
}
if(component.type === 'article-list') {
return <ArticleListComponent params={component.params} openItem={(id) => openComponent('article-detail', {id})} />;
if (component.type === 'article-list') {
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')} />;
}
if(component.type === 'search') {
return <SearchComponent openComponent={openComponent} keyword={component.params!.q} />;
if (component.type === 'search') {
return <SearchComponent openComponent={openComponent} keyword={component.params!.q} />;
}
return <HomeComponent openComponent={openComponent} />;

View File

@@ -1,17 +1,17 @@
import React, {useEffect, useState} from "react";
import React, { useEffect, useState } from "react";
import api from "@/lib/api";
import {HelpItem} from "@/typings";
import { HelpItem } from "@/typings";
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>({});
useEffect(() => {
const _getInfo = async () => {
let result = await api.get('article/info', {id});
if(result.status === 'ok') {
let result = await api.get('article/info', { id });
if (result.status === 'ok') {
setArticle(result.data);
}
};
@@ -21,13 +21,18 @@ const ArticleDetailComponent = ({id, openHome}: {id?: string|number, openHome: (
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>
<h3>{article_info.name}</h3>
<div className="product-detail-content article-detail-content">
<div>{article_info.summary}</div>
<h3>{article_info.name}</h3>
<Comment item_type={'product'} item_id={article_info.id} />
<div className="article-detail-content-ct">{article_info.summary}</div>
<Comment item_type={'product'} item_id={article_info.id} />
</div>
</>
)

View File

@@ -1,8 +1,9 @@
import { List, Pagination} from "antd";
import { List, Pagination, Select} from "antd";
import React, {useEffect, useRef, useState} from "react";
import {HelpItem} from "@/typings";
import api from "@/lib/api";
import Loading from "@/components/Loading";
import { CheckOutlined, CloseOutlined, ArrowLeftOutlined } from '@ant-design/icons';
const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: string|number) => void}) => {
@@ -12,6 +13,7 @@ const TopListComponent = (props: {defaultList: HelpItem[], openItem: (id?: strin
return (
<>
<List
className="Article-list"
itemLayout="horizontal"
dataSource={defaultList}
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}}) => {
@@ -53,8 +70,15 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
}
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
className="help-list-full article-list"
itemLayout="horizontal"
dataSource={item_list.current}
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>
)
}

View File

@@ -1,21 +1,23 @@
import React, {useEffect, useState} from "react";
import React, { useEffect, useState } from "react";
import api from "@/lib/api";
import {HelpItem} from "@/typings";
import {Descriptions} from "antd";
import {formatNumber, showUnixTime} from "@/lib/utils";
import { HelpItem } from "@/typings";
import { Descriptions } from "antd";
import { formatNumber, showUnixTime } from "@/lib/utils";
import PaymentStatus from "@/components/display/PaymentStatus";
import ShippingStatus from "@/components/display/ShippingStatus";
import OrderStatus from "@/components/display/OrderStatus";
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>({});
useEffect(() => {
const _getInfo = async () => {
let result = await api.get('product/info?id=', {id});
if(result.status === 'ok') {
let result = await api.get('product/info?id=', { id });
if (result.status === 'ok') {
setProduct(result.data);
}
};
@@ -24,29 +26,34 @@ const ProductDetailComponent = ({id, openHome}: {id?: string|number, openHome: (
return (
<>
<div><span className={'a-link'} onClick={openHome}>Back to home</span></div>
<Descriptions
bordered
title=" "
size={'small'}
column={1}
>
<Descriptions.Item label="Tên sản phẩm">{product_info.name}</Descriptions.Item>
<Descriptions.Item label="Kho hàng">{product_info.in_stock}</Descriptions.Item>
<Descriptions.Item label="Thời gian cập nhật"> --- </Descriptions.Item>
<Descriptions.Item label="Giá">
{formatNumber(product_info.price)}
</Descriptions.Item>
<Descriptions.Item label="Thông tin">
{product_info.summary}
</Descriptions.Item>
<Descriptions.Item label="Hình ảnh">
<img src={product_info.img} style={{maxWidth: 150, maxHeight: 150}} alt={''} />
</Descriptions.Item>
</Descriptions>
<Comment item_type={'product'} item_id={product_info.id} />
<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
bordered
title=" "
size={'small'}
column={1}
>
<Descriptions.Item label="Tên sản phẩm">{product_info.name}</Descriptions.Item>
<Descriptions.Item label="Kho hàng">{product_info.in_stock}</Descriptions.Item>
<Descriptions.Item label="Thời gian cập nhật"> --- </Descriptions.Item>
<Descriptions.Item label="Giá">
{formatNumber(product_info.price)}
</Descriptions.Item>
<Descriptions.Item label="Thông tin">
{product_info.summary}
</Descriptions.Item>
<Descriptions.Item label="Hình ảnh">
<img src={product_info.img} style={{ maxWidth: 150, maxHeight: 150 }} alt={''} />
</Descriptions.Item>
</Descriptions>
<div className="product-detail-comment">
<Comment item_type={'product'} item_id={product_info.id} />
</div>
</div>
</>
)

View File

@@ -1,30 +1,32 @@
import {Avatar, List, Pagination} from "antd";
import React, {useEffect, useRef, useState} from "react";
import {HelpItem} from "@/typings";
import { Avatar, List, Pagination, Select } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { HelpItem } from "@/typings";
import api from "@/lib/api";
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 stock = item.in_stock ? 'Còn hàng' : 'Hết hàng';
const ProductDescription = ({ item }: { item: HelpItem }) => {
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 (
<>
{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 (
<>
<List
className="product-list"
itemLayout="horizontal"
dataSource={defaultList}
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 item_list = useRef<HelpItem[]>( []);
const SelectBrandFilter = () => {
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 [page, setPage] = useState<number>(1);
@@ -55,22 +83,30 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
useEffect(() => {
(async () => {
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
if(result.status === 'ok') {
if (result.status === 'ok') {
item_list.current = result.data.list;
}
setLoading(false);
}) ();
})();
}, [page, params]);
if(loading) {
if (loading) {
return <Loading />
}
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
className="help-list-full product-list"
itemLayout="horizontal"
dataSource={item_list.current}
renderItem={(item: HelpItem) => (
@@ -78,28 +114,28 @@ const FullListComponent = (props: {openItem: (id?: string|number) => void, param
<List.Item.Meta
avatar={<Avatar src={item.img} />}
title={<span className={'a-link'} >{item.name}</span>}
description={<ProductDescription item={item}/>}
description={<ProductDescription item={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 <FullListComponent params={params} openItem={openItem}/>
return <FullListComponent params={params} openItem={openItem} />
}

View File

@@ -1,6 +1,7 @@
import {Input} from "antd";
import React, {useState} from "react";
import {HelpType} from "@/typings";
import { SettingOutlined } from '@ant-design/icons'
const { Search } = Input;
@@ -18,11 +19,10 @@ const SearchBox = (props: {openComponent: (type: HelpType, params?: { [key: stri
}
return (
<div>
<div className="search-ct-right">
<Search
placeholder="Tìm kiếm sản phẩm hoặc kiến thức"
allowClear
enterButton="Tìm"
size="middle"
onSearch={startSearch}
value={keyword}

View File

@@ -3,18 +3,40 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-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 {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.ant-layout-header {
background: black;
background: #001B34;
padding: 0px;
}
.ant-layout-sider {
@@ -31,7 +53,663 @@ code {
}
/* use for SPAN in place of A tag */
.box-chatboard {
width: 100vw;
height: 100vh;
overflow: hidden;
float: left;
position: relative;
}
.a-link {
cursor: pointer;
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;
}
}