React + Antd 项目配置
parent
8b89757b25
commit
72634ebb91
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/package-lock.json
|
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>聊天式销售平台需求文档</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "global-sales",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.12.8",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"vite": "^4.5.1"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,6 @@
|
||||
.logo {
|
||||
float: left;
|
||||
height: 36px;
|
||||
margin: 16px 24px 16px 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { configure } from 'mobx'
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
import RootStore from '@/stores/Root'
|
||||
import { StoreContext } from '@/stores/StoreContext'
|
||||
import App from '@/views/App'
|
||||
import OrderFollow from '@/views/OrderFollow'
|
||||
|
||||
configure({
|
||||
useProxies: 'ifavailable',
|
||||
enforceActions: 'observed',
|
||||
computedRequiresReaction: true,
|
||||
observableRequiresReaction: false,
|
||||
reactionRequiresObservable: true,
|
||||
disableErrorBoundaries: process.env.NODE_ENV == 'production'
|
||||
})
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <App />,
|
||||
children: [
|
||||
{ index: true, element: <OrderFollow /> }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const rootStore = new RootStore();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<StoreContext.Provider value={rootStore}>
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={() => <div>Loading...</div>}
|
||||
/>
|
||||
</StoreContext.Provider>
|
||||
</React.StrictMode>
|
||||
)
|
@ -0,0 +1,431 @@
|
||||
import { makeAutoObservable, runInAction } from 'mobx'
|
||||
import { fetchJSON, postForm } from '@/utils/request'
|
||||
import { prepareUrl, isNotEmpty, isEmpty } from '@/utils/commons'
|
||||
|
||||
class Order {
|
||||
|
||||
constructor(root) {
|
||||
makeAutoObservable(this, { rootStore: false })
|
||||
this.root = root
|
||||
}
|
||||
|
||||
fetchOptionList() {
|
||||
const fetchCountryUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSys/InfoSys/GetCountryList'
|
||||
const fetchPhotographerUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSys/InfoSys/GetphotographerList'
|
||||
const fetchTagUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSys/InfoSys/GetPhotoTagList'
|
||||
|
||||
const countryPromise = fetchJSON(fetchCountryUrl)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
const countryOptionList = (json?.result ?? []).map((data, index) => {
|
||||
return {
|
||||
value: data.COI_SN,
|
||||
label: data.COI_Name
|
||||
}
|
||||
})
|
||||
countryOptionList.unshift({value:-1, label: '未知'})
|
||||
return countryOptionList
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
|
||||
const cityPromise = this.fetchCityList('')
|
||||
|
||||
const photographerPromise = fetchJSON(fetchPhotographerUrl)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
const photographerOptionList = (json?.result ?? []).map((data, index) => {
|
||||
return {
|
||||
value: data.U_Name,
|
||||
label: data.U_Name
|
||||
}
|
||||
})
|
||||
return photographerOptionList
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
|
||||
const tagPromise = fetchJSON(fetchTagUrl)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
const tagOptionList = (json?.result ?? []).map((data, index) => {
|
||||
return {
|
||||
value: data.Tag_SN,
|
||||
label: data.Tag_Name
|
||||
}
|
||||
})
|
||||
return tagOptionList
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all([countryPromise, cityPromise, photographerPromise, tagPromise])
|
||||
.then(results => {
|
||||
return {
|
||||
countryOptionList: results[0],
|
||||
cityOptionList: results[1],
|
||||
photographerOptionList: results[2],
|
||||
tagOptionList: results[3]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getKeywordHistory() {
|
||||
const keywordListText = this.root.getLocal('KEYWORD_LIST')
|
||||
const keywordList = isEmpty(keywordListText) ? [] : JSON.parse(keywordListText)
|
||||
return keywordList.map(keyword => {
|
||||
return {value: keyword}
|
||||
})
|
||||
}
|
||||
|
||||
fetchCityList(countryId) {
|
||||
const fetchCityUrl =
|
||||
prepareUrl('https://p9axztuwd7x8a7.mycht.cn/service-InfoSys/InfoSys/GetCityList')
|
||||
.append('coi_sn', countryId)
|
||||
.build()
|
||||
return fetchJSON(fetchCityUrl)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
const cityOptionList = (json?.result ?? []).map((data, index) => {
|
||||
return {
|
||||
value: data.CII_SN,
|
||||
label: data.CII_Name
|
||||
}
|
||||
})
|
||||
cityOptionList.unshift({value:-1, label: '未知'})
|
||||
return cityOptionList
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fetchCropImageList(userId) {
|
||||
const formData = new FormData()
|
||||
formData.append('PII_SN', this.selectedImage.PII_SN)
|
||||
formData.append('user_id', userId)
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/search_cutimage'
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
runInAction(() => {
|
||||
this.croppedImageList = json.result
|
||||
})
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleFavorite(image, userId) {
|
||||
runInAction(() => {
|
||||
image.isFavorite = !image.isFavorite
|
||||
})
|
||||
const formData = new FormData()
|
||||
formData.append('PII_SN', image.PII_SN)
|
||||
formData.append('user_id', userId)
|
||||
formData.append('addordelete', image.isFavorite ? 1: 0)
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/favorite_image'
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
console.info(json)
|
||||
// json.result.PII_SN
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fetchImageList(country, city, keyword, tags, star, type, userId) {
|
||||
|
||||
runInAction(() => {
|
||||
this.imageSearchList = []
|
||||
})
|
||||
|
||||
if (isNotEmpty(keyword)) {
|
||||
const keywordListText = this.root.getLocal('KEYWORD_LIST')
|
||||
const keywordList = isEmpty(keywordListText) ? [] : JSON.parse(keywordListText)
|
||||
if (keywordList.indexOf(keyword) == -1) {
|
||||
|
||||
keywordList.unshift(keyword)
|
||||
}
|
||||
if (keywordList.length > 8) {
|
||||
keywordList.splice(8, keywordList.length)
|
||||
}
|
||||
this.root.putLocal('KEYWORD_LIST', JSON.stringify(keywordList))
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('CountrySN', country)
|
||||
formData.append('citySN', city)
|
||||
formData.append('keyword', keyword)
|
||||
formData.append('tags', tags)
|
||||
formData.append('star', star)
|
||||
formData.append('searchType', type)
|
||||
formData.append('user_id', userId)
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/search_image'
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
runInAction(() => {
|
||||
this.imageSearchList = json.result
|
||||
})
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startUpload(userId) {
|
||||
|
||||
for (let index = 0; index < this.imageUploadList.length; index++) {
|
||||
const element = this.imageUploadList[index];
|
||||
this.requiredAlert =
|
||||
(element.country === null) ||
|
||||
(element.city === null) ||
|
||||
(element.description_zh === '') ||
|
||||
(element.description_en === '') ||
|
||||
(element.photographer === '') ||
|
||||
(element.copyright === -1) ||
|
||||
(element.star === -1)
|
||||
|
||||
if (this.requiredAlert) break
|
||||
}
|
||||
|
||||
if (this.requiredAlert) return
|
||||
|
||||
let successCount = 0
|
||||
this.uploadPercent = 0
|
||||
const imageCount = this.imageUploadList.length
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/upload_image'
|
||||
|
||||
this.imageUploadList.forEach((element, index) => {
|
||||
const formData = new FormData()
|
||||
formData.append('image_file', element.image_file)
|
||||
formData.append('image_uid', element.image_uid)
|
||||
formData.append('country', element.country)
|
||||
formData.append('city', element.city)
|
||||
formData.append('description_zh', element.description_zh)
|
||||
formData.append('description_en', element.description_en)
|
||||
formData.append('photographer', element.photographer)
|
||||
formData.append('copyright', element.copyright)
|
||||
formData.append('labels', element.labelValues.join(','))
|
||||
formData.append('user_id', userId)
|
||||
formData.append('star', element.star)
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
successCount++
|
||||
runInAction(() => {
|
||||
this.uploadPercent = parseInt(successCount / imageCount * 100)
|
||||
})
|
||||
if (json.errcode == 0) {
|
||||
runInAction(() => {
|
||||
for (var i = this.imageUploadList.length - 1; i >= 0; i--) {
|
||||
const current = this.imageUploadList[i]
|
||||
if (current.image_uid === json.result.image_uid) {
|
||||
current.success = true
|
||||
current.image_url = 'https://p9axztuwd7x8a7.mycht.cn/service-fileServer' + json.result.image_path
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
startCrop(cropData, resizeWidth, userId, website) {
|
||||
const formData = new FormData()
|
||||
formData.append('filename', this.selectedImage.PII_Location + this.selectedImage.PII_FileName)
|
||||
formData.append('x', cropData.x)
|
||||
formData.append('y', cropData.y)
|
||||
formData.append('width', Math.trunc(cropData.width))
|
||||
formData.append('height', Math.trunc(cropData.height))
|
||||
formData.append('resize_width', resizeWidth)
|
||||
formData.append('user_id', userId)
|
||||
formData.append('webcode', website)
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/crop_image'
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
this.fetchCropImageList()
|
||||
const imageHtml = this.generateHtml(json.result, website)
|
||||
const cropResult = {
|
||||
imageHtml: imageHtml
|
||||
}
|
||||
return cropResult
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getImageHost(website) {
|
||||
let imageHost = ''
|
||||
switch (website) {
|
||||
case 'ch':
|
||||
imageHost = 'https://images.chinahighlights.com'
|
||||
break
|
||||
case 'ah':
|
||||
imageHost = 'https://images.asiahighlights.com'
|
||||
break
|
||||
case 'gh':
|
||||
imageHost = 'https://images.globalhighlights.com'
|
||||
break
|
||||
case 'chinatravel':
|
||||
imageHost = 'https://images.chinatravel.com'
|
||||
break
|
||||
case 'sht':
|
||||
imageHost = 'https://images.shanghaihighlights.com'
|
||||
break
|
||||
case 'ts':
|
||||
imageHost = 'https://images.trainspread.com'
|
||||
break
|
||||
case 'mbj':
|
||||
imageHost = 'https://images.mybeijingchina.com'
|
||||
break
|
||||
default:
|
||||
imageHost = 'https://images.chinahighlights.com'
|
||||
break
|
||||
}
|
||||
return imageHost
|
||||
}
|
||||
|
||||
generateHtml(imageObj, website) {
|
||||
const imageUrl = this.getImageHost(website) + imageObj.PII_Location + imageObj.PII_FileName
|
||||
let imageHtml = ''
|
||||
|
||||
switch (website) {
|
||||
case 'ch':
|
||||
imageHtml = '<div class="infoimage"><img alt="' + imageObj.PII2_Intro + '" class="img-responsive" '+ ' width="' + imageObj.PII_Width + '" height="' + imageObj.PII_Height + '" src="' + imageUrl + '"><span class="infoimagetitle">' + imageObj.PII2_Intro + '</span></div>'
|
||||
break
|
||||
case 'ah':
|
||||
imageHtml = '<div class="infoimage"><img alt="' + imageObj.PII2_Intro + '" class="img-responsive" '+ ' width="' + imageObj.PII_Width + '" height="' + imageObj.PII_Height + '" src="' + imageUrl + '"><span class="photoTxt">' + imageObj.PII2_Intro + '</span></div>'
|
||||
break
|
||||
case 'gh':
|
||||
imageHtml = '<div class="infoimage"><img alt="' + imageObj.PII2_Intro + '" class="img-responsive" '+ ' width="' + imageObj.PII_Width + '" height="' + imageObj.PII_Height + '" src="' + imageUrl + '"><span class="infoimagetitle">' + imageObj.PII2_Intro + '</span></div>'
|
||||
break
|
||||
case 'chinatravel':
|
||||
case 'sht':
|
||||
case 'ts':
|
||||
case 'mbj':
|
||||
imageHtml = '<figure><img alt="' + imageObj.PII2_Intro + '" class="img-responsive" '+ ' width="' + imageObj.PII_Width + '" height="' + imageObj.PII_Height + '" src="' + imageUrl + '"></figure>'
|
||||
break
|
||||
default:
|
||||
imageHtml = '<img alt="' + imageObj.PII2_Intro + '" class="img-responsive" '+ ' width="' + imageObj.PII_Width + '" height="' + imageObj.PII_Height + '" src="' + imageUrl + '">'
|
||||
break
|
||||
}
|
||||
|
||||
return imageHtml
|
||||
}
|
||||
|
||||
useImage(imageId, userId, website) {
|
||||
const formData = new FormData()
|
||||
formData.append('PII_SN', imageId)
|
||||
formData.append('user_id', userId)
|
||||
formData.append('Webcode', website)
|
||||
const postUrl = 'https://p9axztuwd7x8a7.mycht.cn/service-InfoSysSOA/use_image'
|
||||
|
||||
return postForm(postUrl, formData)
|
||||
.then(json => {
|
||||
if (json.errcode == 0) {
|
||||
console.info(json)
|
||||
} else {
|
||||
throw new Error(json.errmsg + ': ' + json.errcode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
continueUpload() {
|
||||
runInAction(() => {
|
||||
this.imageUploadList = []
|
||||
this.uploadPercent = -1
|
||||
})
|
||||
}
|
||||
|
||||
initImageList(fileList) {
|
||||
runInAction(() => {
|
||||
this.imageUploadList = fileList.map(file => {
|
||||
return {
|
||||
image_file: file,
|
||||
image_uid: file.uid,
|
||||
country: null,
|
||||
city: null,
|
||||
description_zh: '',
|
||||
description_en: '',
|
||||
photographer: '',
|
||||
copyright: -1,
|
||||
labels: '',
|
||||
labelValues: [],
|
||||
user_id: -1,
|
||||
star: -1,
|
||||
success: false,
|
||||
image_url: ''
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
removeUploadImage(fileUid) {
|
||||
runInAction(() => {
|
||||
this.imageUploadList = this.imageUploadList.filter((image, index) => {
|
||||
return image.image_uid != fileUid
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
syncProperties() {
|
||||
if (this.imageUploadList.length > 1) {
|
||||
const source = this.imageUploadList[0]
|
||||
runInAction(() => {
|
||||
for (let index = 1; index < this.imageUploadList.length; index++) {
|
||||
const current = this.imageUploadList[index]
|
||||
current.country = source.country
|
||||
current.city = source.city
|
||||
current.description_zh = source.description_zh
|
||||
current.description_en = source.description_en
|
||||
current.photographer = source.photographer
|
||||
current.copyright = source.copyright
|
||||
current.labelValues = source.labelValues
|
||||
current.user_id = source.user_id
|
||||
current.star = source.star
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateImageProperty(uid, name, value) {
|
||||
for (var i = this.imageUploadList.length - 1; i >= 0; i--) {
|
||||
const current = this.imageUploadList[i]
|
||||
if (current.image_file.uid === uid) {
|
||||
current[name] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectImage(image) {
|
||||
this.selectedImage = image
|
||||
}
|
||||
|
||||
imageUploadList = []
|
||||
imageSearchList = []
|
||||
uploadPercent = -1
|
||||
selectedImage = null
|
||||
croppedImageList = []
|
||||
// 上传时数据验证是否通过
|
||||
requiredAlert = false
|
||||
}
|
||||
|
||||
export default Order
|
@ -0,0 +1,60 @@
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import Auth from './Auth'
|
||||
import Order from './Order'
|
||||
|
||||
class Root {
|
||||
constructor() {
|
||||
this.orderStore = new Order(this)
|
||||
this.authStore = new Auth(this)
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
clearSession() {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage
|
||||
sessionStorage.clear()
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!')
|
||||
}
|
||||
}
|
||||
|
||||
getSession(key) {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage
|
||||
return sessionStorage.getItem(key)
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
putSession(key, value) {
|
||||
if (window.sessionStorage) {
|
||||
const sessionStorage = window.sessionStorage
|
||||
return sessionStorage.setItem(key, value)
|
||||
} else {
|
||||
console.error('browser not support sessionStorage!')
|
||||
}
|
||||
}
|
||||
|
||||
getLocal(key) {
|
||||
if (window.localStorage) {
|
||||
const localStorage = window.localStorage
|
||||
return localStorage.getItem(key)
|
||||
} else {
|
||||
console.error('browser not support localStorage!')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
putLocal(key, value) {
|
||||
if (window.localStorage) {
|
||||
const localStorage = window.localStorage
|
||||
return localStorage.setItem(key, value)
|
||||
} else {
|
||||
console.error('browser not support localStorage!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Root
|
@ -0,0 +1,7 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const StoreContext = createContext()
|
||||
|
||||
export function useStore() {
|
||||
return useContext(StoreContext)
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
export function copy(obj) {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
export function splitArray2Parts(arr, size) {
|
||||
const result = []
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
result.push(arr.slice(i, i + size))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function camelCase(name) {
|
||||
return name.substr(0, 1).toLowerCase() + name.substr(1)
|
||||
}
|
||||
|
||||
export class UrlBuilder {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.paramList = []
|
||||
}
|
||||
|
||||
append(name, value) {
|
||||
if (isNotEmpty(value)) {
|
||||
this.paramList.push({ name: name, value: value })
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
build() {
|
||||
this.paramList.forEach((e, i, a) => {
|
||||
if (i === 0) {
|
||||
this.url += "?"
|
||||
} else {
|
||||
this.url += "&"
|
||||
}
|
||||
this.url += e.name + "=" + e.value
|
||||
})
|
||||
return this.url
|
||||
}
|
||||
}
|
||||
|
||||
export function isNotEmpty(val) {
|
||||
return val !== undefined && val !== null && val !== ""
|
||||
}
|
||||
|
||||
export function isEmpty(val) {
|
||||
return val === undefined || val === null || val === ""
|
||||
}
|
||||
|
||||
export function prepareUrl(url) {
|
||||
return new UrlBuilder(url)
|
||||
}
|
||||
|
||||
export function debounce(fn, delay = 500) {
|
||||
let timer
|
||||
return e => {
|
||||
e.persist()
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
fn(e)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
export function throttle(fn, delay, atleast) {
|
||||
let timeout = null,
|
||||
startTime = new Date()
|
||||
return function () {
|
||||
let curTime = new Date()
|
||||
clearTimeout(timeout)
|
||||
if (curTime - startTime >= atleast) {
|
||||
fn()
|
||||
startTime = curTime
|
||||
} else {
|
||||
timeout = setTimeout(fn, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clickUrl(url) {
|
||||
const httpLink = document.createElement("a")
|
||||
httpLink.href = url
|
||||
httpLink.target = "_blank"
|
||||
httpLink.click()
|
||||
}
|
||||
|
||||
export function escape2Html(str) {
|
||||
var temp = document.createElement("div")
|
||||
temp.innerHTML = str
|
||||
var output = temp.innerText || temp.textContent
|
||||
temp = null
|
||||
return output
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
function checkStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
} else {
|
||||
const message =
|
||||
'Fetch error: ' + response.url + ' ' + response.status + ' (' +
|
||||
response.statusText + ')'
|
||||
const error = new Error(message)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchText(url) {
|
||||
return fetch(url)
|
||||
.then(checkStatus)
|
||||
.then(response => response.text())
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchJSON(url) {
|
||||
return fetch(url)
|
||||
.then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function postForm(url, data) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function postJSON(url, obj) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
export function postStream(url, obj) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/octet-stream'
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
import { Outlet, Link, useHref, NavLink } from 'react-router-dom'
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { Layout, Menu, ConfigProvider, theme, Empty, Row, Col, Dropdown, Space, Typography, Result, App as AntApp } from 'antd'
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import '@/assets/App.css'
|
||||
import AppLogo from '@/assets/logo-gh.png'
|
||||
import { useStore } from '@/stores/StoreContext.js'
|
||||
import { isEmpty } from '@/utils/commons'
|
||||
|
||||
const { Header, Footer, Content } = Layout
|
||||
const { Title } = Typography
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: <Link to="/account/change-password">Change password</Link>,
|
||||
key: "0",
|
||||
},
|
||||
{
|
||||
label: <Link to="/account/profile">Profile</Link>,
|
||||
key: "1",
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
label: <Link to="/login?out">Logout</Link>,
|
||||
key: "3",
|
||||
},
|
||||
];
|
||||
function App() {
|
||||
|
||||
const href = useHref()
|
||||
const { authStore } = useStore()
|
||||
const { userId, website } = authStore
|
||||
const shouldBeLogin = (isEmpty(userId) || isEmpty(website)) && (href.indexOf('/authorise/') == -1)
|
||||
let defaultPath = 'follow'
|
||||
|
||||
if (href !== '/') {
|
||||
const splitPath = href.split('/')
|
||||
|
||||
if (splitPath.length > 1) {
|
||||
defaultPath = splitPath[2]
|
||||
}
|
||||
}
|
||||
const {
|
||||
token: { colorBgContainer },
|
||||
} = theme.useToken()
|
||||
|
||||
function globalEmpty() {
|
||||
return (
|
||||
<Empty description={false} />
|
||||
)
|
||||
}
|
||||
|
||||
function renderLogin() {
|
||||
return (
|
||||
<Result
|
||||
status='403'
|
||||
title='授权失败'
|
||||
subTitle='请登陆信息平台,通过指定链接打开。'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderLayout() {
|
||||
return (
|
||||
<Layout>
|
||||
<Header className='header' style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%', background: 'white' }}>
|
||||
<Row gutter={{ md: 24 }} align='middle'>
|
||||
<Col span={5}>
|
||||
<NavLink to='/'>
|
||||
<img src={AppLogo} className='logo' alt='App logo' />
|
||||
</NavLink>
|
||||
<Title level={3}>
|
||||
聊天式销售平台
|
||||
</Title>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Menu
|
||||
mode='horizontal'
|
||||
selectedKeys={[defaultPath]}
|
||||
items={[
|
||||
{ key: 'follow', label: <Link to='/order/follow'>订单跟踪</Link> },
|
||||
{ key: 'chat', label: <Link to='/order/chat'>销售聊天</Link> },
|
||||
{ key: 'history', label: <Link to='/chat/history'>聊天历史</Link> },
|
||||
{ key: 'management', label: <Link to='/sales/management'>销售管理</Link> }
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={9} style={{ color: "white", marginBottom: "0", display: "flex", justifyContent: "end" }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: <Link to="/account/profile">个人资料</Link>,
|
||||
key: "1",
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
label: <Link to="/login?out">退出</Link>,
|
||||
key: "3",
|
||||
},
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<a onClick={(e) => e.preventDefault()}>
|
||||
<Space>
|
||||
廖一军
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</a>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Layout>
|
||||
<Content
|
||||
style={{
|
||||
padding: 24,
|
||||
margin: 0,
|
||||
minHeight: 280,
|
||||
background: colorBgContainer,
|
||||
}}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
<Footer>桂林海纳国际旅行社有限公司</Footer>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#645822',
|
||||
borderRadius: 4
|
||||
},
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}
|
||||
renderEmpty={globalEmpty}
|
||||
>
|
||||
<AntApp>
|
||||
{renderLayout()}
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
@ -0,0 +1,154 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react'
|
||||
import { Row, Col, Divider, Table , Card, Button, Input,
|
||||
Space, Empty, Radio, Select, AutoComplete, Spin, Typography, Flex
|
||||
} from 'antd'
|
||||
import {
|
||||
StarFilled, ZoomInOutlined, StarOutlined, SearchOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
const dataSource = [
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
orderNumber: 'LU231218115(3)',
|
||||
fullname: 'Giacomo Guilizzoni(R1)',
|
||||
orderStatus: '新订单',
|
||||
trip: 'Itinerary2: Tkyoto tour',
|
||||
lastMessage: '2024-03-25 16:02',
|
||||
comment: '吃素、蜜月',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'orderNumber',
|
||||
key: 'orderNumber',
|
||||
},
|
||||
{
|
||||
title: '客人姓名',
|
||||
dataIndex: 'fullname',
|
||||
key: 'fullname',
|
||||
},
|
||||
{
|
||||
title: '订单状态',
|
||||
dataIndex: 'orderStatus',
|
||||
key: 'orderStatus',
|
||||
},
|
||||
{
|
||||
title: '报价title',
|
||||
dataIndex: 'trip',
|
||||
key: 'trip',
|
||||
},
|
||||
{
|
||||
title: '客人最后一次回复时间',
|
||||
dataIndex: 'lastMessage',
|
||||
key: 'lastMessage',
|
||||
},
|
||||
{
|
||||
title: '附加信息',
|
||||
dataIndex: 'comment',
|
||||
key: 'comment',
|
||||
},
|
||||
];
|
||||
|
||||
function OrderFollow() {
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Spin spinning={false} delay={500}>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
<Row gutter={[16, 16]} justify='start' align='middle'>
|
||||
<Col span={24}>
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ label: '今日任务', value: 'today' },
|
||||
{ label: '潜力客户', value: 'myused' },
|
||||
{ label: '重点订单', value: 'myupload' },
|
||||
{ label: '成行', value: 'myfavorites' },
|
||||
{ label: '走团中', value: 'unCheck' },
|
||||
{ label: '走团后一月', value: 'unCheck' },
|
||||
{ label: '高级查询', value: 'unCheck' }
|
||||
]}
|
||||
value={'today'}
|
||||
onChange={({ target: { value } }) => {
|
||||
setSearchType(value)
|
||||
}}
|
||||
optionType='button'
|
||||
buttonStyle='solid'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
<Divider plain orientation='left'></Divider>
|
||||
<Space
|
||||
direction='vertical'
|
||||
size='middle'
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Table dataSource={dataSource} columns={columns} />
|
||||
</Space>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(OrderFollow)
|
@ -0,0 +1,44 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
manifest: true,
|
||||
chunkSizeWarningLimit: 555,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
// if (id.includes('antd')) {
|
||||
// console.info('chunk: ' + id)
|
||||
// return 'antd-component'
|
||||
// } else if (id.includes('rc-')) {
|
||||
// return 'rc-component'
|
||||
// } else if (id.includes('ant-design')) {
|
||||
// return 'ant-design'
|
||||
// } else {
|
||||
// // console.info('chunk: ' + id)
|
||||
// }
|
||||
if (id.includes('node_modules')) {
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
}
|
||||
},
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : [];
|
||||
// const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]';
|
||||
return `assets/[name].[hash].js`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue