客服:根据代码评审进行 fix
parent
9c03966e12
commit
a4a8548b5a
2
.env
2
.env
|
@ -5,7 +5,7 @@ SHOPRO_VERSION = v1.8.3
|
||||||
SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
|
SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
|
||||||
|
|
||||||
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
|
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
|
||||||
SHOPRO_DEV_BASE_URL = http://192.168.1.105:48080
|
SHOPRO_DEV_BASE_URL = http://127.0.0.1:48080
|
||||||
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
|
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
|
||||||
|
|
||||||
# 后端接口前缀(一般不建议调整)
|
# 后端接口前缀(一般不建议调整)
|
||||||
|
|
448
manifest.json
448
manifest.json
|
@ -1,227 +1,239 @@
|
||||||
{
|
{
|
||||||
"name" : "芋道商城",
|
"name": "芋道商城",
|
||||||
"appid" : "__UNI__460BC4C",
|
"appid": "__UNI__460BC4C",
|
||||||
"description" : "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
|
"description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
|
||||||
"versionName" : "2.1.0",
|
"versionName": "2.1.0",
|
||||||
"versionCode" : 183,
|
"versionCode": 183,
|
||||||
"transformPx" : false,
|
"transformPx": false,
|
||||||
"app-plus" : {
|
"app-plus": {
|
||||||
"usingComponents" : true,
|
"usingComponents": true,
|
||||||
"nvueCompiler" : "uni-app",
|
"nvueCompiler": "uni-app",
|
||||||
"nvueStyleCompiler" : "uni-app",
|
"nvueStyleCompiler": "uni-app",
|
||||||
"compilerVersion" : 3,
|
"compilerVersion": 3,
|
||||||
"nvueLaunchMode" : "fast",
|
"nvueLaunchMode": "fast",
|
||||||
"splashscreen" : {
|
"splashscreen": {
|
||||||
"alwaysShowBeforeRender" : true,
|
"alwaysShowBeforeRender": true,
|
||||||
"waiting" : true,
|
"waiting": true,
|
||||||
"autoclose" : true,
|
"autoclose": true,
|
||||||
"delay" : 0
|
"delay": 0
|
||||||
|
},
|
||||||
|
"safearea": {
|
||||||
|
"bottom": {
|
||||||
|
"offset": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
"Payment": {},
|
||||||
|
"Share": {},
|
||||||
|
"VideoPlayer": {},
|
||||||
|
"OAuth": {}
|
||||||
|
},
|
||||||
|
"distribute": {
|
||||||
|
"android": {
|
||||||
|
"permissions": [
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_SMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
|
||||||
|
],
|
||||||
|
"minSdkVersion": 21,
|
||||||
|
"schemes": "shopro"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"urlschemewhitelist": [
|
||||||
|
"baidumap",
|
||||||
|
"iosamap"
|
||||||
|
],
|
||||||
|
"dSYMs": false,
|
||||||
|
"privacyDescription": {
|
||||||
|
"NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
|
||||||
|
"NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
|
||||||
|
"NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
|
||||||
|
"NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
|
||||||
},
|
},
|
||||||
"safearea" : {
|
"urltypes": "shopro",
|
||||||
"bottom" : {
|
"capabilities": {
|
||||||
"offset" : "none"
|
"entitlements": {
|
||||||
}
|
"com.apple.developer.associated-domains": [
|
||||||
|
"applinks:shopro.sheepjs.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"modules" : {
|
"idfa": true
|
||||||
"Payment" : {},
|
},
|
||||||
"Share" : {},
|
"sdkConfigs": {
|
||||||
"VideoPlayer" : {},
|
"speech": {
|
||||||
"OAuth" : {}
|
"ifly": {}
|
||||||
},
|
},
|
||||||
"distribute" : {
|
"ad": {},
|
||||||
"android" : {
|
"oauth": {
|
||||||
"permissions" : [
|
"apple": {},
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
"weixin": {
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
"appid": "wxae7a0c156da9383b",
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
|
"payment": {
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
"weixin": {
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
"__platform__": [
|
||||||
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
|
"ios",
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
"android"
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
],
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
"appid": "wxae7a0c156da9383b",
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
||||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
|
"alipay": {
|
||||||
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
"__platform__": [
|
||||||
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
"ios",
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
"android"
|
||||||
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
|
]
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
},
|
||||||
"<uses-permission android:name=\"android.permission.READ_SMS\"/>",
|
"share": {
|
||||||
"<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
|
"weixin": {
|
||||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
"appid": "wxae7a0c156da9383b",
|
||||||
"<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
|
"UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
||||||
"<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
|
}
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
|
|
||||||
],
|
|
||||||
"minSdkVersion" : 21,
|
|
||||||
"schemes" : "shopro"
|
|
||||||
},
|
|
||||||
"ios" : {
|
|
||||||
"urlschemewhitelist" : [ "baidumap", "iosamap" ],
|
|
||||||
"dSYMs" : false,
|
|
||||||
"privacyDescription" : {
|
|
||||||
"NSPhotoLibraryUsageDescription" : "需要同意访问您的相册选取图片才能完善该条目",
|
|
||||||
"NSPhotoLibraryAddUsageDescription" : "需要同意访问您的相册才能保存该图片",
|
|
||||||
"NSCameraUsageDescription" : "需要同意访问您的摄像头拍摄照片才能完善该条目",
|
|
||||||
"NSUserTrackingUsageDescription" : "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
|
|
||||||
},
|
|
||||||
"urltypes" : "shopro",
|
|
||||||
"capabilities" : {
|
|
||||||
"entitlements" : {
|
|
||||||
"com.apple.developer.associated-domains" : [ "applinks:shopro.sheepjs.com" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idfa" : true
|
|
||||||
},
|
|
||||||
"sdkConfigs" : {
|
|
||||||
"speech" : {
|
|
||||||
"ifly" : {}
|
|
||||||
},
|
|
||||||
"ad" : {},
|
|
||||||
"oauth" : {
|
|
||||||
"apple" : {},
|
|
||||||
"weixin" : {
|
|
||||||
"appid" : "wxae7a0c156da9383b",
|
|
||||||
"UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"payment" : {
|
|
||||||
"weixin" : {
|
|
||||||
"__platform__" : [ "ios", "android" ],
|
|
||||||
"appid" : "wxae7a0c156da9383b",
|
|
||||||
"UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
|
||||||
},
|
|
||||||
"alipay" : {
|
|
||||||
"__platform__" : [ "ios", "android" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"share" : {
|
|
||||||
"weixin" : {
|
|
||||||
"appid" : "wxae7a0c156da9383b",
|
|
||||||
"UniversalLinks" : "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"orientation" : [ "portrait-primary" ],
|
|
||||||
"splashscreen" : {
|
|
||||||
"androidStyle" : "common",
|
|
||||||
"iosStyle" : "common",
|
|
||||||
"useOriginalMsgbox" : true
|
|
||||||
},
|
|
||||||
"icons" : {
|
|
||||||
"android" : {
|
|
||||||
"hdpi" : "unpackage/res/icons/72x72.png",
|
|
||||||
"xhdpi" : "unpackage/res/icons/96x96.png",
|
|
||||||
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
|
||||||
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
|
||||||
},
|
|
||||||
"ios" : {
|
|
||||||
"appstore" : "unpackage/res/icons/1024x1024.png",
|
|
||||||
"ipad" : {
|
|
||||||
"app" : "unpackage/res/icons/76x76.png",
|
|
||||||
"app@2x" : "unpackage/res/icons/152x152.png",
|
|
||||||
"notification" : "unpackage/res/icons/20x20.png",
|
|
||||||
"notification@2x" : "unpackage/res/icons/40x40.png",
|
|
||||||
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
|
||||||
"settings" : "unpackage/res/icons/29x29.png",
|
|
||||||
"settings@2x" : "unpackage/res/icons/58x58.png",
|
|
||||||
"spotlight" : "unpackage/res/icons/40x40.png",
|
|
||||||
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
|
||||||
},
|
|
||||||
"iphone" : {
|
|
||||||
"app@2x" : "unpackage/res/icons/120x120.png",
|
|
||||||
"app@3x" : "unpackage/res/icons/180x180.png",
|
|
||||||
"notification@2x" : "unpackage/res/icons/40x40.png",
|
|
||||||
"notification@3x" : "unpackage/res/icons/60x60.png",
|
|
||||||
"settings@2x" : "unpackage/res/icons/58x58.png",
|
|
||||||
"settings@3x" : "unpackage/res/icons/87x87.png",
|
|
||||||
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
|
||||||
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"quickapp" : {},
|
"orientation": [
|
||||||
"quickapp-native" : {
|
"portrait-primary"
|
||||||
"icon" : "/static/logo.png",
|
],
|
||||||
"package" : "com.example.demo",
|
"splashscreen": {
|
||||||
"features" : [
|
"androidStyle": "common",
|
||||||
{
|
"iosStyle": "common",
|
||||||
"name" : "system.clipboard"
|
"useOriginalMsgbox": true
|
||||||
}
|
},
|
||||||
]
|
"icons": {
|
||||||
},
|
"android": {
|
||||||
"quickapp-webview" : {
|
"hdpi": "unpackage/res/icons/72x72.png",
|
||||||
"icon" : "/static/logo.png",
|
"xhdpi": "unpackage/res/icons/96x96.png",
|
||||||
"package" : "com.example.demo",
|
"xxhdpi": "unpackage/res/icons/144x144.png",
|
||||||
"minPlatformVersion" : 1070,
|
"xxxhdpi": "unpackage/res/icons/192x192.png"
|
||||||
"versionName" : "1.0.0",
|
|
||||||
"versionCode" : 100
|
|
||||||
},
|
|
||||||
"mp-weixin" : {
|
|
||||||
"appid" : "wx98df718e528399d2",
|
|
||||||
"setting" : {
|
|
||||||
"urlCheck" : false,
|
|
||||||
"minified" : true,
|
|
||||||
"postcss" : true
|
|
||||||
},
|
},
|
||||||
"optimization" : {
|
"ios": {
|
||||||
"subPackages" : true
|
"appstore": "unpackage/res/icons/1024x1024.png",
|
||||||
},
|
"ipad": {
|
||||||
"plugins" : {},
|
"app": "unpackage/res/icons/76x76.png",
|
||||||
"lazyCodeLoading" : "requiredComponents",
|
"app@2x": "unpackage/res/icons/152x152.png",
|
||||||
"usingComponents" : {},
|
"notification": "unpackage/res/icons/20x20.png",
|
||||||
"permission" : {},
|
"notification@2x": "unpackage/res/icons/40x40.png",
|
||||||
"requiredPrivateInfos" : [ "chooseAddress" ]
|
"proapp@2x": "unpackage/res/icons/167x167.png",
|
||||||
},
|
"settings": "unpackage/res/icons/29x29.png",
|
||||||
"mp-alipay" : {
|
"settings@2x": "unpackage/res/icons/58x58.png",
|
||||||
"usingComponents" : true
|
"spotlight": "unpackage/res/icons/40x40.png",
|
||||||
},
|
"spotlight@2x": "unpackage/res/icons/80x80.png"
|
||||||
"mp-baidu" : {
|
},
|
||||||
"usingComponents" : true
|
"iphone": {
|
||||||
},
|
"app@2x": "unpackage/res/icons/120x120.png",
|
||||||
"mp-toutiao" : {
|
"app@3x": "unpackage/res/icons/180x180.png",
|
||||||
"usingComponents" : true
|
"notification@2x": "unpackage/res/icons/40x40.png",
|
||||||
},
|
"notification@3x": "unpackage/res/icons/60x60.png",
|
||||||
"mp-jd" : {
|
"settings@2x": "unpackage/res/icons/58x58.png",
|
||||||
"usingComponents" : true
|
"settings@3x": "unpackage/res/icons/87x87.png",
|
||||||
},
|
"spotlight@2x": "unpackage/res/icons/80x80.png",
|
||||||
"h5" : {
|
"spotlight@3x": "unpackage/res/icons/120x120.png"
|
||||||
"template" : "index.html",
|
}
|
||||||
"router" : {
|
|
||||||
"mode" : "history",
|
|
||||||
"base" : "./"
|
|
||||||
},
|
|
||||||
"sdkConfigs" : {
|
|
||||||
"maps" : {}
|
|
||||||
},
|
|
||||||
"async" : {
|
|
||||||
"timeout" : 20000
|
|
||||||
},
|
|
||||||
"title" : "芋道商城",
|
|
||||||
"optimization" : {
|
|
||||||
"treeShaking" : {
|
|
||||||
"enable" : true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devServer" : {
|
|
||||||
"port" : 8383
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickapp": {},
|
||||||
|
"quickapp-native": {
|
||||||
|
"icon": "/static/logo.png",
|
||||||
|
"package": "com.example.demo",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"name": "system.clipboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"quickapp-webview": {
|
||||||
|
"icon": "/static/logo.png",
|
||||||
|
"package": "com.example.demo",
|
||||||
|
"minPlatformVersion": 1070,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"versionCode": 100
|
||||||
|
},
|
||||||
|
"mp-weixin": {
|
||||||
|
"appid": "wx63c280fe3248a3e7",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false,
|
||||||
|
"minified": true,
|
||||||
|
"postcss": true
|
||||||
},
|
},
|
||||||
"vueVersion" : "3",
|
"optimization": {
|
||||||
"_spaceID" : "192b4892-5452-4e1d-9f09-eee1ece40639",
|
"subPackages": true
|
||||||
"locale" : "zh-Hans",
|
},
|
||||||
"fallbackLocale" : "zh-Hans"
|
"plugins": {},
|
||||||
|
"lazyCodeLoading": "requiredComponents",
|
||||||
|
"usingComponents": {},
|
||||||
|
"permission": {},
|
||||||
|
"requiredPrivateInfos": [
|
||||||
|
"chooseAddress"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mp-alipay": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-baidu": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-toutiao": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-jd": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"h5": {
|
||||||
|
"template": "index.html",
|
||||||
|
"router": {
|
||||||
|
"mode": "hash",
|
||||||
|
"base": "./"
|
||||||
|
},
|
||||||
|
"sdkConfigs": {
|
||||||
|
"maps": {}
|
||||||
|
},
|
||||||
|
"async": {
|
||||||
|
"timeout": 20000
|
||||||
|
},
|
||||||
|
"title": "芋道商城",
|
||||||
|
"optimization": {
|
||||||
|
"treeShaking": {
|
||||||
|
"enable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vueVersion": "3",
|
||||||
|
"_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
|
||||||
|
"locale": "zh-Hans",
|
||||||
|
"fallbackLocale": "zh-Hans"
|
||||||
}
|
}
|
||||||
|
|
27
pages.json
27
pages.json
|
@ -86,8 +86,7 @@
|
||||||
],
|
],
|
||||||
"subPackages": [{
|
"subPackages": [{
|
||||||
"root": "pages/goods",
|
"root": "pages/goods",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "index",
|
"path": "index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "商品详情"
|
"navigationBarTitleText": "商品详情"
|
||||||
|
@ -151,8 +150,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/order",
|
"root": "pages/order",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "detail",
|
"path": "detail",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "订单详情"
|
"navigationBarTitleText": "订单详情"
|
||||||
|
@ -251,8 +249,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/user",
|
"root": "pages/user",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "info",
|
"path": "info",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的信息"
|
"navigationBarTitleText": "我的信息"
|
||||||
|
@ -338,8 +335,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/commission",
|
"root": "pages/commission",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "index",
|
"path": "index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "分销"
|
"navigationBarTitleText": "分销"
|
||||||
|
@ -436,8 +432,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/app",
|
"root": "pages/app",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "sign",
|
"path": "sign",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "签到中心"
|
"navigationBarTitleText": "签到中心"
|
||||||
|
@ -452,8 +447,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/public",
|
"root": "pages/public",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "setting",
|
"path": "setting",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "系统设置"
|
"navigationBarTitleText": "系统设置"
|
||||||
|
@ -502,8 +496,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/coupon",
|
"root": "pages/coupon",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "list",
|
"path": "list",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "领券中心"
|
"navigationBarTitleText": "领券中心"
|
||||||
|
@ -545,8 +538,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/pay",
|
"root": "pages/pay",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "index",
|
"path": "index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "收银台"
|
"navigationBarTitleText": "收银台"
|
||||||
|
@ -586,8 +578,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/activity",
|
"root": "pages/activity",
|
||||||
"pages": [
|
"pages": [{
|
||||||
{
|
|
||||||
"path": "groupon/detail",
|
"path": "groupon/detail",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "拼团详情"
|
"navigationBarTitleText": "拼团详情"
|
||||||
|
|
|
@ -133,11 +133,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
|
||||||
* uniapp 实现虚拟列表
|
|
||||||
*
|
|
||||||
* see https://juejin.cn/post/7105280477141041183
|
|
||||||
*/
|
|
||||||
import { nextTick, reactive, ref, unref } from 'vue';
|
import { nextTick, reactive, ref, unref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import sheep from '@/sheep';
|
import sheep from '@/sheep';
|
||||||
|
@ -170,7 +165,7 @@
|
||||||
|
|
||||||
// 获得消息分页列表
|
// 获得消息分页列表
|
||||||
const getMessageList = async (pageNo = undefined) => {
|
const getMessageList = async (pageNo = undefined) => {
|
||||||
const { data } = await KeFuApi.getMessageListPage({
|
const { data } = await KeFuApi.getKefuMessagePage({
|
||||||
pageNo: pageNo || currentShowPage.value,
|
pageNo: pageNo || currentShowPage.value,
|
||||||
});
|
});
|
||||||
if (isEmpty(data.list)) {
|
if (isEmpty(data.list)) {
|
||||||
|
|
|
@ -1,456 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="chat-box" :style="{ height: pageHeight + 'px' }">
|
|
||||||
<!-- 竖向滚动区域需要设置固定 height -->
|
|
||||||
<scroll-view
|
|
||||||
:style="{ height: pageHeight + 'px' }"
|
|
||||||
scroll-y="true"
|
|
||||||
:scroll-with-animation="false"
|
|
||||||
:enable-back-to-top="true"
|
|
||||||
:scroll-into-view="state.scrollInto"
|
|
||||||
>
|
|
||||||
<!-- 消息渲染 -->
|
|
||||||
<view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
|
|
||||||
<view class="ss-flex ss-row-center ss-col-center">
|
|
||||||
<!-- 日期 -->
|
|
||||||
<view v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)"
|
|
||||||
class="date-message">
|
|
||||||
{{ formatDate(item.date) }}
|
|
||||||
</view>
|
|
||||||
<!-- 系统消息 -->
|
|
||||||
<view v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM" class="system-message">
|
|
||||||
{{ item.content }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- 消息体渲染管理员消息和用户消息并左右展示 -->
|
|
||||||
<view
|
|
||||||
v-if="item.contentType !== KeFuMessageContentTypeEnum.SYSTEM"
|
|
||||||
class="ss-flex ss-col-top"
|
|
||||||
:class="[
|
|
||||||
item.senderType === UserTypeEnum.ADMIN
|
|
||||||
? `ss-row-left`
|
|
||||||
: item.senderType === UserTypeEnum.MEMBER
|
|
||||||
? `ss-row-right`
|
|
||||||
: '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<!-- 客服头像 -->
|
|
||||||
<image
|
|
||||||
v-show="item.senderType === UserTypeEnum.ADMIN"
|
|
||||||
class="chat-avatar ss-m-r-24"
|
|
||||||
:src="
|
|
||||||
sheep.$url.cdn(item?.senderAvatar) ||
|
|
||||||
sheep.$url.static('/static/img/shop/chat/default.png')
|
|
||||||
"
|
|
||||||
mode="aspectFill"
|
|
||||||
></image>
|
|
||||||
|
|
||||||
<!-- 发送状态 -->
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
item.senderType === UserTypeEnum.MEMBER &&
|
|
||||||
index == chatList.length - 1 &&
|
|
||||||
isSendSuccess !== 0
|
|
||||||
"
|
|
||||||
class="send-status"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
v-if="isSendSuccess == -1"
|
|
||||||
class="loading"
|
|
||||||
:src="sheep.$url.static('/static/img/shop/chat/loading.png')"
|
|
||||||
mode="aspectFill"
|
|
||||||
></image>
|
|
||||||
<!-- <image
|
|
||||||
v-if="chatData.isSendSuccess == 1"
|
|
||||||
class="warning"
|
|
||||||
:src="sheep.$url.static('/static/img/shop/chat/warning.png')"
|
|
||||||
mode="aspectFill"
|
|
||||||
@click="onAgainSendMessage(item)"
|
|
||||||
></image> -->
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- 内容 -->
|
|
||||||
<template v-if="item.contentType === KeFuMessageContentTypeEnum.TEXT">
|
|
||||||
<view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}">
|
|
||||||
<mp-html :content="replaceEmoji(item.content)" />
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.contentType === KeFuMessageContentTypeEnum.IMAGE">
|
|
||||||
<view class="message-box" :class="{'admin': item.senderType === UserTypeEnum.ADMIN}" :style="{ width: '200rpx' }">
|
|
||||||
<su-image
|
|
||||||
class="message-img"
|
|
||||||
isPreview
|
|
||||||
:previewList="[sheep.$url.cdn(item.content)]"
|
|
||||||
:current="0"
|
|
||||||
:src="sheep.$url.cdn(item.content)"
|
|
||||||
:height="200"
|
|
||||||
:width="200"
|
|
||||||
mode="aspectFill"
|
|
||||||
></su-image>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.contentType === KeFuMessageContentTypeEnum.PRODUCT">
|
|
||||||
<GoodsItem
|
|
||||||
:goodsData="item.content.item"
|
|
||||||
@tap="
|
|
||||||
sheep.$router.go('/pages/goods/index', {
|
|
||||||
id: item.content.item.id,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.contentType === KeFuMessageContentTypeEnum.ORDER">
|
|
||||||
<OrderItem
|
|
||||||
from="msg"
|
|
||||||
:orderData="item.content.item"
|
|
||||||
@tap="
|
|
||||||
sheep.$router.go('/pages/order/detail', {
|
|
||||||
id: item.content.item.id,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<!-- user头像 -->
|
|
||||||
<image
|
|
||||||
v-if="item.senderType === UserTypeEnum.MEMBER"
|
|
||||||
class="chat-avatar ss-m-l-24"
|
|
||||||
:src="sheep.$url.cdn(item?.senderAvatar) ||
|
|
||||||
sheep.$url.static('/static/img/shop/chat/default.png')"
|
|
||||||
mode="aspectFill"
|
|
||||||
>
|
|
||||||
</image>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- 视图滚动锚点 -->
|
|
||||||
<view id="scrollBottom"></view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import sheep from '@/sheep';
|
|
||||||
import OrderItem from '@/pages/chat/components/order.vue';
|
|
||||||
import GoodsItem from '@/pages/chat/components/goods.vue';
|
|
||||||
import { reactive, ref, unref } from 'vue';
|
|
||||||
import { formatDate } from '@/sheep/util';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { KeFuMessageContentTypeEnum,UserTypeEnum } from './constants';
|
|
||||||
import { emojiList } from '@/pages/chat/emoji';
|
|
||||||
|
|
||||||
const KEFU_MESSAGE_TYPE = 'kefu_message_type'; // 客服消息类型
|
|
||||||
const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
|
|
||||||
const pageHeight = safeArea.height - 44 - 35 - 50;
|
|
||||||
const state = reactive({
|
|
||||||
scrollInto: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const chatList = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
conversationId: 1001,
|
|
||||||
senderId: 1,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 2,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
|
||||||
content: "Hello, how are you?",
|
|
||||||
readStatus: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
conversationId: 1001,
|
|
||||||
senderId: 2,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 1,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
|
||||||
content: "I'm good, thanks! [流泪][流泪][流泪][流泪]",
|
|
||||||
readStatus: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
conversationId: 1002,
|
|
||||||
senderId: 3,
|
|
||||||
senderType: 2, // UserTypeEnum.ADMIN
|
|
||||||
receiverId: 4,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 2, // KeFuMessageContentTypeEnum.IMAGE
|
|
||||||
content: "https://static.iocoder.cn/mall/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg",
|
|
||||||
readStatus: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
conversationId: 1002,
|
|
||||||
senderId: 4,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 3,
|
|
||||||
receiverType: 2, // UserTypeEnum.ADMIN
|
|
||||||
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
|
||||||
content: "This is a text message.",
|
|
||||||
readStatus: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
conversationId: 1003,
|
|
||||||
senderId: 5,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 6,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 3, // KeFuMessageContentTypeEnum.VOICE
|
|
||||||
content: "Voice content here",
|
|
||||||
readStatus: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
conversationId: 1003,
|
|
||||||
senderId: 6,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 5,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 1, // KeFuMessageContentTypeEnum.TEXT
|
|
||||||
content: "Another text message.",
|
|
||||||
readStatus: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
conversationId: 1004,
|
|
||||||
senderId: 7,
|
|
||||||
senderType: 2, // UserTypeEnum.ADMIN
|
|
||||||
receiverId: 8,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 1, // KeFuMessageContentTypeEnum.VIDEO
|
|
||||||
content: "Video content here",
|
|
||||||
readStatus: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
conversationId: 1004,
|
|
||||||
senderId: 8,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 7,
|
|
||||||
receiverType: 2, // UserTypeEnum.ADMIN
|
|
||||||
contentType: 5, // KeFuMessageContentTypeEnum.SYSTEM
|
|
||||||
content: "System message content",
|
|
||||||
readStatus: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
conversationId: 1005,
|
|
||||||
senderId: 9,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 10,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 10, // KeFuMessageContentTypeEnum.PRODUCT
|
|
||||||
content: "Product message content",
|
|
||||||
readStatus: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
conversationId: 1005,
|
|
||||||
senderId: 10,
|
|
||||||
senderType: 1, // UserTypeEnum.MEMBER
|
|
||||||
receiverId: 9,
|
|
||||||
receiverType: 1, // UserTypeEnum.MEMBER
|
|
||||||
contentType: 11, // KeFuMessageContentTypeEnum.ORDER
|
|
||||||
content: "Order message content",
|
|
||||||
readStatus: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const isSendSuccess = ref(-1)
|
|
||||||
//======================= 工具函数 =======================
|
|
||||||
/**
|
|
||||||
* 是否显示时间
|
|
||||||
* @param {*} item - 数据
|
|
||||||
* @param {*} index - 索引
|
|
||||||
*/
|
|
||||||
const showTime = (item, index) => {
|
|
||||||
if (unref(chatList)[index + 1]) {
|
|
||||||
let dateString = dayjs(unref(chatList)[index + 1].date).fromNow();
|
|
||||||
return dateString !== dayjs(unref(item).date).fromNow();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// 处理表情
|
|
||||||
function replaceEmoji(data) {
|
|
||||||
let newData = data;
|
|
||||||
if (typeof newData !== 'object') {
|
|
||||||
let reg = /\[(.+?)\]/g; // [] 中括号
|
|
||||||
let zhEmojiName = newData.match(reg);
|
|
||||||
if (zhEmojiName) {
|
|
||||||
zhEmojiName.forEach((item) => {
|
|
||||||
let emojiFile = selEmojiFile(item);
|
|
||||||
newData = newData.replace(
|
|
||||||
item,
|
|
||||||
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
|
|
||||||
'/static/img/chat/emoji/' + emojiFile,
|
|
||||||
)}"/>`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newData;
|
|
||||||
}
|
|
||||||
function selEmojiFile(name) {
|
|
||||||
for (let index in emojiList) {
|
|
||||||
if (emojiList[index].name === name) {
|
|
||||||
return emojiList[index].file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.chat-box {
|
|
||||||
padding: 0 20rpx 0;
|
|
||||||
|
|
||||||
.loadmore-btn {
|
|
||||||
width: 98%;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
|
|
||||||
.loadmore-icon {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item {
|
|
||||||
margin-bottom: 33rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-message,
|
|
||||||
.system-message {
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
background-color: var(--ui-BG-3);
|
|
||||||
color: #999;
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-avatar {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-status {
|
|
||||||
color: #333;
|
|
||||||
height: 80rpx;
|
|
||||||
margin-right: 8rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
-webkit-animation: rotating 2s linear infinite;
|
|
||||||
animation: rotating 2s linear infinite;
|
|
||||||
|
|
||||||
@-webkit-keyframes rotating {
|
|
||||||
0% {
|
|
||||||
transform: rotateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotating {
|
|
||||||
0% {
|
|
||||||
transform: rotateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
color: #ff3000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-box {
|
|
||||||
max-width: 50%;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
// max-width: 500rpx;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
word-wrap: break-word;
|
|
||||||
padding: 20rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
|
||||||
|
|
||||||
&.admin {
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep() {
|
|
||||||
.imgred {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imgred,
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep() {
|
|
||||||
.goods,
|
|
||||||
.order {
|
|
||||||
max-width: 500rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-img {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-wrap {
|
|
||||||
// width: 100%;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 29rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: var(--ui-BG-Main);
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-img {
|
|
||||||
width: 400rpx;
|
|
||||||
height: 400rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
#scrollBottom {
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -61,7 +61,7 @@
|
||||||
contentType: KeFuMessageContentTypeEnum.TEXT,
|
contentType: KeFuMessageContentTypeEnum.TEXT,
|
||||||
content: chat.msg,
|
content: chat.msg,
|
||||||
};
|
};
|
||||||
await KeFuApi.sendMessage(data);
|
await KeFuApi.sendKefuMessage(data);
|
||||||
await getMessageList()
|
await getMessageList()
|
||||||
chat.msg = '';
|
chat.msg = '';
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,678 +0,0 @@
|
||||||
<template>
|
|
||||||
<s-layout class="chat-wrap" title="客服" navbar="inner">
|
|
||||||
<div class="status">
|
|
||||||
{{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
|
|
||||||
</div>
|
|
||||||
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
|
|
||||||
<view class="chat-box" :style="{ height: pageHeight + 'px' }">
|
|
||||||
<scroll-view
|
|
||||||
:style="{ height: pageHeight + 'px' }"
|
|
||||||
scroll-y="true"
|
|
||||||
:scroll-with-animation="false"
|
|
||||||
:enable-back-to-top="true"
|
|
||||||
:scroll-into-view="chat.scrollInto"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="loadmore-btn ss-reset-button"
|
|
||||||
v-if="
|
|
||||||
chatList.length &&
|
|
||||||
chatHistoryPagination.lastPage > 1 &&
|
|
||||||
loadingMap[chatHistoryPagination.loadStatus].title
|
|
||||||
"
|
|
||||||
@click="onLoadMore"
|
|
||||||
>
|
|
||||||
{{ loadingMap[chatHistoryPagination.loadStatus].title }}
|
|
||||||
<i
|
|
||||||
class="loadmore-icon sa-m-l-6"
|
|
||||||
:class="loadingMap[chatHistoryPagination.loadStatus].icon"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
|
|
||||||
<view class="ss-flex ss-row-center ss-col-center">
|
|
||||||
<!-- 日期 -->
|
|
||||||
<view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">
|
|
||||||
{{ formatTime(item.date) }}
|
|
||||||
</view>
|
|
||||||
<!-- 系统消息 -->
|
|
||||||
<view v-if="item.from === 'system'" class="system-message">
|
|
||||||
{{ item.content.text }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<!-- 常见问题 -->
|
|
||||||
<view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">
|
|
||||||
<view class="title">猜你想问</view>
|
|
||||||
<view
|
|
||||||
class="item"
|
|
||||||
v-for="(item, index) in item.content.list"
|
|
||||||
:key="index"
|
|
||||||
@click="onTemplateList(item)"
|
|
||||||
>
|
|
||||||
* {{ item.title }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view
|
|
||||||
v-if="
|
|
||||||
(item.from === 'customer_service' && item.mode !== 'template') ||
|
|
||||||
item.from === 'customer'
|
|
||||||
"
|
|
||||||
class="ss-flex ss-col-top"
|
|
||||||
:class="[
|
|
||||||
item.from === 'customer_service'
|
|
||||||
? `ss-row-left`
|
|
||||||
: item.from === 'customer'
|
|
||||||
? `ss-row-right`
|
|
||||||
: '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<!-- 客服头像 -->
|
|
||||||
<image
|
|
||||||
v-show="item.from === 'customer_service'"
|
|
||||||
class="chat-avatar ss-m-r-24"
|
|
||||||
:src="
|
|
||||||
sheep.$url.cdn(item?.sender?.avatar) ||
|
|
||||||
sheep.$url.static('/static/img/shop/chat/default.png')
|
|
||||||
"
|
|
||||||
mode="aspectFill"
|
|
||||||
></image>
|
|
||||||
|
|
||||||
<!-- 发送状态 -->
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
item.from === 'customer' &&
|
|
||||||
index == chatData.chatList.length - 1 &&
|
|
||||||
chatData.isSendSucces !== 0
|
|
||||||
"
|
|
||||||
class="send-status"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
v-if="chatData.isSendSucces == -1"
|
|
||||||
class="loading"
|
|
||||||
:src="sheep.$url.static('/static/img/shop/chat/loading.png')"
|
|
||||||
mode="aspectFill"
|
|
||||||
></image>
|
|
||||||
<!-- <image
|
|
||||||
v-if="chatData.isSendSucces == 1"
|
|
||||||
class="warning"
|
|
||||||
:src="sheep.$url.static('/static/img/shop/chat/warning.png')"
|
|
||||||
mode="aspectFill"
|
|
||||||
@click="onAgainSendMessage(item)"
|
|
||||||
></image> -->
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- 内容 -->
|
|
||||||
<template v-if="item.mode === 'text'">
|
|
||||||
<view class="message-box" :class="[item.from]">
|
|
||||||
<div
|
|
||||||
class="message-text ss-flex ss-flex-wrap"
|
|
||||||
@click="onRichtext"
|
|
||||||
v-html="replaceEmoji(item.content.text)"
|
|
||||||
></div>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.mode === 'image'">
|
|
||||||
<view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">
|
|
||||||
<su-image
|
|
||||||
class="message-img"
|
|
||||||
isPreview
|
|
||||||
:previewList="[sheep.$url.cdn(item.content.url)]"
|
|
||||||
:current="0"
|
|
||||||
:src="sheep.$url.cdn(item.content.url)"
|
|
||||||
:height="200"
|
|
||||||
:width="200"
|
|
||||||
mode="aspectFill"
|
|
||||||
></su-image>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.mode === 'goods'">
|
|
||||||
<GoodsItem
|
|
||||||
:goodsData="item.content.item"
|
|
||||||
@tap="
|
|
||||||
sheep.$router.go('/pages/goods/index', {
|
|
||||||
id: item.content.item.id,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-if="item.mode === 'order'">
|
|
||||||
<OrderItem
|
|
||||||
from="msg"
|
|
||||||
:orderData="item.content.item"
|
|
||||||
@tap="
|
|
||||||
sheep.$router.go('/pages/order/detail', {
|
|
||||||
id: item.content.item.id,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<!-- user头像 -->
|
|
||||||
<image
|
|
||||||
v-show="item.from === 'customer'"
|
|
||||||
class="chat-avatar ss-m-l-24"
|
|
||||||
:src="sheep.$url.cdn(customerUserInfo.avatar)"
|
|
||||||
mode="aspectFill"
|
|
||||||
>
|
|
||||||
</image>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view id="scrollBottom"></view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
|
||||||
<su-fixed bottom>
|
|
||||||
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
|
||||||
</su-fixed>
|
|
||||||
<!-- 聊天工具 -->
|
|
||||||
<tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose"
|
|
||||||
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
|
|
||||||
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
|
|
||||||
</tools-popup>
|
|
||||||
|
|
||||||
<SelectPopup
|
|
||||||
:mode="chat.selectMode"
|
|
||||||
:show="chat.showSelect"
|
|
||||||
@select="onSelect"
|
|
||||||
@close="chat.showSelect = false"
|
|
||||||
/>
|
|
||||||
</s-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import sheep from '@/sheep';
|
|
||||||
import { computed, reactive, toRefs } from 'vue';
|
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
|
||||||
import { emojiList } from './emoji.js';
|
|
||||||
import SelectPopup from './components/select-popup.vue';
|
|
||||||
import GoodsItem from './components/goods.vue';
|
|
||||||
import OrderItem from './components/order.vue';
|
|
||||||
import MessageInput from './components/messageInput.vue';
|
|
||||||
import ToolsPopup from './components/toolsPopup.vue';
|
|
||||||
import { useChatWebSocket } from './socket';
|
|
||||||
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
|
|
||||||
|
|
||||||
const {
|
|
||||||
socketInit,
|
|
||||||
state: chatData,
|
|
||||||
socketSendMsg,
|
|
||||||
formatChatInput,
|
|
||||||
socketHistoryList,
|
|
||||||
onDrop,
|
|
||||||
onPaste,
|
|
||||||
getFocus,
|
|
||||||
// upload,
|
|
||||||
getUserToken,
|
|
||||||
// socketTest,
|
|
||||||
showTime,
|
|
||||||
formatTime,
|
|
||||||
} = useChatWebSocket();
|
|
||||||
const chatList = toRefs(chatData).chatList;
|
|
||||||
const customerServiceInfo = toRefs(chatData).customerServerInfo;
|
|
||||||
const chatHistoryPagination = toRefs(chatData).chatHistoryPagination;
|
|
||||||
const customerUserInfo = toRefs(chatData).customerUserInfo;
|
|
||||||
const socketState = toRefs(chatData).socketState;
|
|
||||||
|
|
||||||
const sys_navBar = sheep.$platform.navbar;
|
|
||||||
const chatConfig = computed(() => sheep.$store('app').chat);
|
|
||||||
|
|
||||||
const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
|
|
||||||
const pageHeight = safeArea.height - 44 - 35 - 50;
|
|
||||||
|
|
||||||
const chatStatus = {
|
|
||||||
online: {
|
|
||||||
text: '在线',
|
|
||||||
colorVariate: '#46c55f',
|
|
||||||
},
|
|
||||||
offline: {
|
|
||||||
text: '离线',
|
|
||||||
colorVariate: '#b5b5b5',
|
|
||||||
},
|
|
||||||
busy: {
|
|
||||||
text: '忙碌',
|
|
||||||
colorVariate: '#ff0e1b',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const loadingMap = {
|
|
||||||
loadmore: {
|
|
||||||
title: '查看更多',
|
|
||||||
icon: 'el-icon-d-arrow-left',
|
|
||||||
},
|
|
||||||
nomore: {
|
|
||||||
title: '没有更多了',
|
|
||||||
icon: '',
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
title: '加载中... ',
|
|
||||||
icon: 'el-icon-loading',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const onLoadMore = () => {
|
|
||||||
chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();
|
|
||||||
};
|
|
||||||
|
|
||||||
const chat = reactive({
|
|
||||||
msg: '',
|
|
||||||
scrollInto: '',
|
|
||||||
|
|
||||||
showTools: false,
|
|
||||||
toolsMode: '',
|
|
||||||
|
|
||||||
showSelect: false,
|
|
||||||
selectMode: '',
|
|
||||||
chatStyle: {
|
|
||||||
mode: 'inner',
|
|
||||||
color: '#F8270F',
|
|
||||||
type: 'color',
|
|
||||||
alwaysShow: 1,
|
|
||||||
src: '',
|
|
||||||
list: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//======================= 聊天工具相关 =======================
|
|
||||||
|
|
||||||
function handleToolsClose() {
|
|
||||||
chat.showTools = false;
|
|
||||||
chat.toolsMode = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEmoji(item) {
|
|
||||||
chat.msg += item.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击工具栏开关
|
|
||||||
function onTools(mode) {
|
|
||||||
// if (!socketState.value.isConnect) {
|
|
||||||
// sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!chat.toolsMode || chat.toolsMode === mode) {
|
|
||||||
chat.showTools = !chat.showTools;
|
|
||||||
}
|
|
||||||
chat.toolsMode = mode;
|
|
||||||
if (!chat.showTools) {
|
|
||||||
chat.toolsMode = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShowSelect(mode) {
|
|
||||||
chat.showTools = false;
|
|
||||||
chat.showSelect = true;
|
|
||||||
chat.selectMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSelect({ type, data }) {
|
|
||||||
let msg = '';
|
|
||||||
switch (type) {
|
|
||||||
case 'image':
|
|
||||||
const { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');
|
|
||||||
msg = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'image',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
url: fullurl,
|
|
||||||
path: path,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'goods':
|
|
||||||
msg = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'goods',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
item: {
|
|
||||||
id: data.goods.id,
|
|
||||||
title: data.goods.title,
|
|
||||||
image: data.goods.image,
|
|
||||||
price: data.goods.price,
|
|
||||||
stock: data.goods.stock,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'order':
|
|
||||||
msg = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'order',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
item: {
|
|
||||||
id: data.id,
|
|
||||||
order_sn: data.order_sn,
|
|
||||||
create_time: data.create_time,
|
|
||||||
pay_fee: data.pay_fee,
|
|
||||||
items: data.items.filter((item) => ({
|
|
||||||
goods_id: item.goods_id,
|
|
||||||
goods_title: item.goods_title,
|
|
||||||
goods_image: item.goods_image,
|
|
||||||
goods_price: item.goods_price,
|
|
||||||
})),
|
|
||||||
status_text: data.status_text,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (msg) {
|
|
||||||
socketSendMsg(msg, () => {
|
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
// scrollBottom();
|
|
||||||
chat.showTools = false;
|
|
||||||
chat.showSelect = false;
|
|
||||||
chat.selectMode = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAgainSendMessage(item) {
|
|
||||||
if (!socketState.value.isConnect) {
|
|
||||||
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!item) return;
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'text',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: item.content,
|
|
||||||
};
|
|
||||||
socketSendMsg(data, () => {
|
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSendMessage() {
|
|
||||||
if (!socketState.value.isConnect) {
|
|
||||||
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!chat.msg) return;
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'text',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
text: chat.msg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
socketSendMsg(data, () => {
|
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
chat.showTools = false;
|
|
||||||
// scrollBottom();
|
|
||||||
setTimeout(() => {
|
|
||||||
chat.msg = '';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击猜你想问
|
|
||||||
function onTemplateList(e) {
|
|
||||||
if (!socketState.value.isConnect) {
|
|
||||||
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'text',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
text: e.title,
|
|
||||||
},
|
|
||||||
customData: {
|
|
||||||
question_id: e.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
socketSendMsg(data, () => {
|
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
// scrollBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selEmojiFile(name) {
|
|
||||||
for (let index in emojiList) {
|
|
||||||
if (emojiList[index].name === name) {
|
|
||||||
return emojiList[index].file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceEmoji(data) {
|
|
||||||
let newData = data;
|
|
||||||
if (typeof newData !== 'object') {
|
|
||||||
let reg = /\[(.+?)\]/g; // [] 中括号
|
|
||||||
let zhEmojiName = newData.match(reg);
|
|
||||||
if (zhEmojiName) {
|
|
||||||
zhEmojiName.forEach((item) => {
|
|
||||||
let emojiFile = selEmojiFile(item);
|
|
||||||
newData = newData.replace(
|
|
||||||
item,
|
|
||||||
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
|
|
||||||
'/static/img/chat/emoji/' + emojiFile,
|
|
||||||
)}"/>`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollBottom() {
|
|
||||||
let timeout = null;
|
|
||||||
chat.scrollInto = '';
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
chat.scrollInto = 'scrollBottom';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
const websocket = useWebSocket()
|
|
||||||
onLoad(async () => {
|
|
||||||
websocket.socketInit({}, () => {
|
|
||||||
scrollBottom();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.page-bg {
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
|
||||||
background-size: 750rpx 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-wrap {
|
|
||||||
// :deep() {
|
|
||||||
// .ui-navbar-box {
|
|
||||||
// background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
.status {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 3;
|
|
||||||
height: 70rpx;
|
|
||||||
padding: 0 30rpx;
|
|
||||||
background: var(--ui-BG-Main-opacity-1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--ui-BG-Main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-box {
|
|
||||||
padding: 0 20rpx 0;
|
|
||||||
|
|
||||||
.loadmore-btn {
|
|
||||||
width: 98%;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
|
|
||||||
.loadmore-icon {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item {
|
|
||||||
margin-bottom: 33rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-message,
|
|
||||||
.system-message {
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
background-color: var(--ui-BG-3);
|
|
||||||
color: #999;
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-avatar {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-status {
|
|
||||||
color: #333;
|
|
||||||
height: 80rpx;
|
|
||||||
margin-right: 8rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
-webkit-animation: rotating 2s linear infinite;
|
|
||||||
animation: rotating 2s linear infinite;
|
|
||||||
|
|
||||||
@-webkit-keyframes rotating {
|
|
||||||
0% {
|
|
||||||
transform: rotateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotating {
|
|
||||||
0% {
|
|
||||||
transform: rotateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotateZ(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
color: #ff3000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-box {
|
|
||||||
max-width: 50%;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
// max-width: 500rpx;
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
word-wrap: break-word;
|
|
||||||
padding: 20rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
|
||||||
|
|
||||||
&.customer_service {
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep() {
|
|
||||||
.imgred {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.imgred,
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep() {
|
|
||||||
.goods,
|
|
||||||
.order {
|
|
||||||
max-width: 500rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-img {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-wrap {
|
|
||||||
// width: 100%;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 29rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: var(--ui-BG-Main);
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-img {
|
|
||||||
width: 400rpx;
|
|
||||||
height: 400rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
#scrollBottom {
|
|
||||||
height: 120rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.chat-img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-img {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,818 +0,0 @@
|
||||||
import { reactive, ref, unref } from 'vue';
|
|
||||||
import sheep from '@/sheep';
|
|
||||||
// import chat from '@/sheep/api/chat';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import io from '@hyoga/uni-socket.io';
|
|
||||||
import { baseUrl, websocketPath } from '@/sheep/config';
|
|
||||||
|
|
||||||
export function useChatWebSocket(socketConfig) {
|
|
||||||
let SocketIo = null;
|
|
||||||
|
|
||||||
// chat状态数据
|
|
||||||
const state = reactive({
|
|
||||||
chatDotNum: 0, //总状态红点
|
|
||||||
chatList: [], //会话信息
|
|
||||||
customerUserInfo: {}, //用户信息
|
|
||||||
customerServerInfo: {
|
|
||||||
//客服信息
|
|
||||||
title: '客服已接入',
|
|
||||||
state: 'connecting',
|
|
||||||
avatar: null,
|
|
||||||
nickname: '',
|
|
||||||
},
|
|
||||||
socketState: {
|
|
||||||
isConnect: true, //是否连接成功
|
|
||||||
isConnecting: false, //重连中,不允许新的socket开启。
|
|
||||||
tip: '',
|
|
||||||
},
|
|
||||||
chatHistoryPagination: {
|
|
||||||
page: 0, //当前页
|
|
||||||
list_rows: 10, //每页条数
|
|
||||||
last_id: 0, //最后条ID
|
|
||||||
lastPage: 0, //总共多少页
|
|
||||||
loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
|
|
||||||
},
|
|
||||||
templateChatList: [], //猜你想问
|
|
||||||
|
|
||||||
chatConfig: {}, // 配置信息
|
|
||||||
|
|
||||||
isSendSuccess: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接初始化
|
|
||||||
* @param {Object} config - 配置信息
|
|
||||||
* @param {Function} callBack -回调函数,有新消息接入,保持底部
|
|
||||||
*/
|
|
||||||
const socketInit = (config, callBack) => {
|
|
||||||
state.chatConfig = config;
|
|
||||||
if (SocketIo && SocketIo.connected) return; // 如果socket已经连接,返回false
|
|
||||||
if (state.socketState.isConnecting) return; // 重连中,返回false
|
|
||||||
|
|
||||||
// 启动初始化
|
|
||||||
SocketIo = io(baseUrl + websocketPath, {
|
|
||||||
path:websocketPath,
|
|
||||||
query:{
|
|
||||||
token: getAccessToken()
|
|
||||||
},
|
|
||||||
reconnection: true, // 默认 true 是否断线重连
|
|
||||||
reconnectionAttempts: 5, // 默认无限次 断线尝试次数
|
|
||||||
reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
|
|
||||||
reconnectionDelayMax: 5000, // 默认 5000, 重新连接等待的最长时间 默认 5000
|
|
||||||
randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间
|
|
||||||
timeout: 20000, // 默认 20s
|
|
||||||
transports: ['websocket', 'polling'], // websocket | polling,
|
|
||||||
...config,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听连接
|
|
||||||
SocketIo.on('connect', async (res) => {
|
|
||||||
socketReset(callBack);
|
|
||||||
// socket连接
|
|
||||||
// 用户登录
|
|
||||||
// 顾客登录
|
|
||||||
console.log('socket:connect');
|
|
||||||
});
|
|
||||||
// 监听消息
|
|
||||||
SocketIo.on('message', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
const { message, sender } = res.data;
|
|
||||||
state.chatList.push(formatMessage(res.data.message));
|
|
||||||
|
|
||||||
// 告诉父级页面
|
|
||||||
// window.parent.postMessage({
|
|
||||||
// chatDotNum: ++state.chatDotNum
|
|
||||||
// })
|
|
||||||
callBack && callBack();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听客服接入成功
|
|
||||||
SocketIo.on('customer_service_access', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: res.data.customer_service.name,
|
|
||||||
state: 'online',
|
|
||||||
avatar: res.data.customer_service.avatar,
|
|
||||||
});
|
|
||||||
state.chatList.push(formatMessage(res.data.message));
|
|
||||||
// callBack && callBack()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听排队等待
|
|
||||||
SocketIo.on('waiting_queue', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: res.data.title,
|
|
||||||
state: 'waiting',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
// callBack && callBack()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听没有客服在线
|
|
||||||
SocketIo.on('no_customer_service', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: '暂无客服在线...',
|
|
||||||
state: 'waiting',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
state.chatList.push(formatMessage(res.data.message));
|
|
||||||
// callBack && callBack()
|
|
||||||
});
|
|
||||||
// 监听客服上线
|
|
||||||
SocketIo.on('customer_service_online', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: res.data.customer_service.name,
|
|
||||||
state: 'online',
|
|
||||||
avatar: res.data.customer_service.avatar,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听客服下线
|
|
||||||
SocketIo.on('customer_service_offline', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: res.data.customer_service.name,
|
|
||||||
state: 'offline',
|
|
||||||
avatar: res.data.customer_service.avatar,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听客服忙碌
|
|
||||||
SocketIo.on('customer_service_busy', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: res.data.customer_service.name,
|
|
||||||
state: 'busy',
|
|
||||||
avatar: res.data.customer_service.avatar,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 监听客服断开链接
|
|
||||||
SocketIo.on('customer_service_break', (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: '客服服务结束',
|
|
||||||
state: 'offline',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
state.socketState.isConnect = false;
|
|
||||||
state.socketState.tip = '当前服务已结束';
|
|
||||||
}
|
|
||||||
state.chatList.push(formatMessage(res.data.message));
|
|
||||||
// callBack && callBack()
|
|
||||||
});
|
|
||||||
// 监听自定义错误 custom_error
|
|
||||||
SocketIo.on('custom_error', (error) => {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: error.msg,
|
|
||||||
state: 'offline',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
console.log('custom_error:', error);
|
|
||||||
});
|
|
||||||
// 监听错误 error
|
|
||||||
SocketIo.on('error', (error) => {
|
|
||||||
console.log('error:', error);
|
|
||||||
});
|
|
||||||
// 重连失败 connect_error
|
|
||||||
SocketIo.on('connect_error', (error) => {
|
|
||||||
console.log('connect_error');
|
|
||||||
});
|
|
||||||
// 连接上,但无反应 connect_timeout
|
|
||||||
SocketIo.on('connect_timeout', (error) => {
|
|
||||||
console.log(error, 'connect_timeout');
|
|
||||||
});
|
|
||||||
// 服务进程销毁 disconnect
|
|
||||||
SocketIo.on('disconnect', (error) => {
|
|
||||||
console.log(error, 'disconnect');
|
|
||||||
});
|
|
||||||
// 服务重启重连上reconnect
|
|
||||||
SocketIo.on('reconnect', (error) => {
|
|
||||||
console.log(error, 'reconnect');
|
|
||||||
});
|
|
||||||
// 开始重连reconnect_attempt
|
|
||||||
SocketIo.on('reconnect_attempt', (error) => {
|
|
||||||
state.socketState.isConnect = false;
|
|
||||||
state.socketState.isConnecting = true;
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: `重连中,第${error}次尝试...`,
|
|
||||||
state: 'waiting',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
console.log(error, 'reconnect_attempt');
|
|
||||||
});
|
|
||||||
// 重新连接中reconnecting
|
|
||||||
SocketIo.on('reconnecting', (error) => {
|
|
||||||
console.log(error, 'reconnecting');
|
|
||||||
});
|
|
||||||
// 重新连接错误reconnect_error
|
|
||||||
SocketIo.on('reconnect_error', (error) => {
|
|
||||||
console.log('reconnect_error');
|
|
||||||
});
|
|
||||||
// 重新连接失败reconnect_failed
|
|
||||||
SocketIo.on('reconnect_failed', (error) => {
|
|
||||||
state.socketState.isConnecting = false;
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: `重连失败,请刷新重试~`,
|
|
||||||
state: 'waiting',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
console.log(error, 'reconnect_failed');
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
state.isSendSucces = 1;
|
|
||||||
// }, 500)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置socket
|
|
||||||
const socketReset = (callBack) => {
|
|
||||||
state.chatList = [];
|
|
||||||
state.chatHistoryList = [];
|
|
||||||
state.chatHistoryPagination = {
|
|
||||||
page: 0,
|
|
||||||
per_page: 10,
|
|
||||||
last_id: 0,
|
|
||||||
totalPage: 0,
|
|
||||||
};
|
|
||||||
socketConnection(callBack); // 连接
|
|
||||||
};
|
|
||||||
|
|
||||||
// 退出连接
|
|
||||||
const socketClose = () => {
|
|
||||||
SocketIo.emit('customer_logout', {}, (res) => {
|
|
||||||
console.log('socket:退出', res);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 测试事件
|
|
||||||
const socketTest = () => {
|
|
||||||
SocketIo.emit('test', {}, (res) => {
|
|
||||||
console.log('test:test', res);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
const socketSendMsg = (data, sendMsgCallBack) => {
|
|
||||||
state.isSendSucces = -1;
|
|
||||||
state.chatList.push(data);
|
|
||||||
sendMsgCallBack && sendMsgCallBack();
|
|
||||||
SocketIo.emit(
|
|
||||||
'message',
|
|
||||||
{
|
|
||||||
message: formatInput(data),
|
|
||||||
...data.customData,
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
// setTimeout(() => {
|
|
||||||
state.isSendSucces = res.error;
|
|
||||||
// }, 500)
|
|
||||||
|
|
||||||
// console.log(res, 'socket:send');
|
|
||||||
// sendMsgCallBack && sendMsgCallBack()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 连接socket,存入sessionId
|
|
||||||
const socketConnection = (callBack) => {
|
|
||||||
SocketIo.emit(
|
|
||||||
'connection',
|
|
||||||
{
|
|
||||||
auth: 'user',
|
|
||||||
token: uni.getStorageSync('socketUserToken') || '',
|
|
||||||
session_id: uni.getStorageSync('socketSessionId') || '',
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
socketCustomerLogin(callBack);
|
|
||||||
uni.setStorageSync('socketSessionId', res.data.session_id);
|
|
||||||
// uni.getStorageSync('socketUserToken') && socketLogin(uni.getStorageSync(
|
|
||||||
// 'socketUserToken')) // 如果有用户token,绑定
|
|
||||||
state.customerUserInfo = res.data.chat_user;
|
|
||||||
state.socketState.isConnect = true;
|
|
||||||
} else {
|
|
||||||
editCustomerServerInfo({
|
|
||||||
title: `服务器异常!`,
|
|
||||||
state: 'waiting',
|
|
||||||
avatar: '',
|
|
||||||
});
|
|
||||||
state.socketState.isConnect = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取token
|
|
||||||
const getAccessToken = () => {
|
|
||||||
return uni.getStorageSync('token');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用户登录
|
|
||||||
const socketLogin = (token) => {
|
|
||||||
SocketIo.emit(
|
|
||||||
'login',
|
|
||||||
{
|
|
||||||
token: token,
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
console.log(res, 'socket:login');
|
|
||||||
state.customerUserInfo = res.data.chat_user;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 顾客登录
|
|
||||||
const socketCustomerLogin = (callBack) => {
|
|
||||||
SocketIo.emit(
|
|
||||||
'customer_login',
|
|
||||||
{
|
|
||||||
room_id: state.chatConfig.room_id,
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
state.templateChatList = res.data.questions.length ? res.data.questions : [];
|
|
||||||
state.chatList.push({
|
|
||||||
from: 'customer_service', // 用户customer右 | 顾客customer_service左 | 系统system中间
|
|
||||||
mode: 'template', // goods,order,image,text,system
|
|
||||||
date: new Date().getTime(), //时间
|
|
||||||
content: {
|
|
||||||
//内容
|
|
||||||
list: state.templateChatList,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.error === 0 && socketHistoryList(callBack);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取历史消息
|
|
||||||
const socketHistoryList = (historyCallBack) => {
|
|
||||||
state.chatHistoryPagination.loadStatus = 'loading';
|
|
||||||
state.chatHistoryPagination.page += 1;
|
|
||||||
SocketIo.emit('messages', state.chatHistoryPagination, (res) => {
|
|
||||||
if (res.error === 0) {
|
|
||||||
state.chatHistoryPagination.total = res.data.messages.total;
|
|
||||||
state.chatHistoryPagination.lastPage = res.data.messages.last_page;
|
|
||||||
state.chatHistoryPagination.page = res.data.messages.current_page;
|
|
||||||
res.data.messages.data.forEach((item) => {
|
|
||||||
item.message_type && state.chatList.unshift(formatMessage(item));
|
|
||||||
});
|
|
||||||
state.chatHistoryPagination.loadStatus =
|
|
||||||
state.chatHistoryPagination.page < state.chatHistoryPagination.lastPage
|
|
||||||
? 'loadmore'
|
|
||||||
: 'nomore';
|
|
||||||
if (state.chatHistoryPagination.last_id == 0) {
|
|
||||||
state.chatHistoryPagination.last_id = res.data.messages.data.length
|
|
||||||
? res.data.messages.data[0].id
|
|
||||||
: 0;
|
|
||||||
}
|
|
||||||
state.chatHistoryPagination.page === 1 && historyCallBack && historyCallBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 历史记录之后,猜你想问
|
|
||||||
// state.chatList.push({
|
|
||||||
// from: 'customer_service', // 用户customer右 | 顾客customer_service左 | 系统system中间
|
|
||||||
// mode: 'template', // goods,order,image,text,system
|
|
||||||
// date: new Date().getTime(), //时间
|
|
||||||
// content: { //内容
|
|
||||||
// list: state.templateChatList
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 修改客服信息
|
|
||||||
const editCustomerServerInfo = (data) => {
|
|
||||||
state.customerServerInfo = {
|
|
||||||
...state.customerServerInfo,
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ================
|
|
||||||
* 工具函数 ↓
|
|
||||||
* ===============
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否显示时间
|
|
||||||
* @param {*} item - 数据
|
|
||||||
* @param {*} index - 索引
|
|
||||||
*/
|
|
||||||
const showTime = (item, index) => {
|
|
||||||
if (unref(state.chatList)[index + 1]) {
|
|
||||||
let dateString = dayjs(unref(state.chatList)[index + 1].date).fromNow();
|
|
||||||
if (dateString === dayjs(unref(item).date).fromNow()) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
dateString = dayjs(unref(item).date).fromNow();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化时间
|
|
||||||
* @param {*} time - 时间戳
|
|
||||||
*/
|
|
||||||
const formatTime = (time) => {
|
|
||||||
let diffTime = new Date().getTime() - time;
|
|
||||||
if (diffTime > 28 * 24 * 60 * 1000) {
|
|
||||||
return dayjs(time).format('MM/DD HH:mm');
|
|
||||||
}
|
|
||||||
if (diffTime > 360 * 28 * 24 * 60 * 1000) {
|
|
||||||
return dayjs(time).format('YYYY/MM/DD HH:mm');
|
|
||||||
}
|
|
||||||
return dayjs(time).fromNow();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取焦点
|
|
||||||
* @param {*} virtualNode - 节点信息 ref
|
|
||||||
*/
|
|
||||||
const getFocus = (virtualNode) => {
|
|
||||||
if (window.getSelection) {
|
|
||||||
let chatInput = unref(virtualNode);
|
|
||||||
chatInput.focus();
|
|
||||||
let range = window.getSelection();
|
|
||||||
range.selectAllChildren(chatInput);
|
|
||||||
range.collapseToEnd();
|
|
||||||
} else if (document.selection) {
|
|
||||||
let range = document.selection.createRange();
|
|
||||||
range.moveToElementText(chatInput);
|
|
||||||
range.collapse(false);
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件上传
|
|
||||||
* @param {Blob} file -文件数据流
|
|
||||||
* @return {path,fullPath}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const upload = (name, file) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let data = new FormData();
|
|
||||||
data.append('file', file, name);
|
|
||||||
data.append('group', 'chat');
|
|
||||||
ajax({
|
|
||||||
url: '/upload',
|
|
||||||
method: 'post',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
success: function (res) {
|
|
||||||
resolve(res);
|
|
||||||
},
|
|
||||||
error: function (err) {
|
|
||||||
reject(err);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 粘贴到输入框
|
|
||||||
* @param {*} e - 粘贴内容
|
|
||||||
* @param {*} uploadHttp - 上传图片地址
|
|
||||||
*/
|
|
||||||
const onPaste = async (e) => {
|
|
||||||
let paste = e.clipboardData || window.clipboardData;
|
|
||||||
let filesArr = Array.from(paste.files);
|
|
||||||
filesArr.forEach(async (child) => {
|
|
||||||
if (child && child.type.includes('image')) {
|
|
||||||
e.preventDefault(); //阻止默认
|
|
||||||
let file = child;
|
|
||||||
const img = await readImg(file);
|
|
||||||
const blob = await compressImg(img, file.type);
|
|
||||||
const { data } = await upload(file.name, blob);
|
|
||||||
let image = `<img class="full-url" src='${data.fullurl}'>`;
|
|
||||||
document.execCommand('insertHTML', false, image);
|
|
||||||
} else {
|
|
||||||
document.execCommand('insertHTML', false, paste.getData('text'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拖拽到输入框
|
|
||||||
* @param {*} e - 粘贴内容
|
|
||||||
* @param {*} uploadHttp - 上传图片地址
|
|
||||||
*/
|
|
||||||
const onDrop = async (e) => {
|
|
||||||
e.preventDefault(); //阻止默认
|
|
||||||
let filesArr = Array.from(e.dataTransfer.files);
|
|
||||||
filesArr.forEach(async (child) => {
|
|
||||||
if (child && child.type.includes('image')) {
|
|
||||||
let file = child;
|
|
||||||
const img = await readImg(file);
|
|
||||||
const blob = await compressImg(img, file.type);
|
|
||||||
const { data } = await upload(file.name, blob);
|
|
||||||
let image = `<img class="full-url" src='${data.fullurl}' >`;
|
|
||||||
document.execCommand('insertHTML', false, image);
|
|
||||||
} else {
|
|
||||||
ElMessage({
|
|
||||||
message: '禁止拖拽非图片资源',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析富文本输入框内容
|
|
||||||
* @param {*} virtualNode -节点信息
|
|
||||||
* @param {Function} formatInputCallBack - cb 回调
|
|
||||||
*/
|
|
||||||
const formatChatInput = (virtualNode, formatInputCallBack) => {
|
|
||||||
let res = '';
|
|
||||||
let elemArr = Array.from(virtualNode.childNodes);
|
|
||||||
elemArr.forEach((child, index) => {
|
|
||||||
if (child.nodeName === '#text') {
|
|
||||||
//如果为文本节点
|
|
||||||
res += child.nodeValue;
|
|
||||||
if (
|
|
||||||
//文本节点的后面是图片,并且不是emoji,分开发送。输入框中的图片和文本表情分开。
|
|
||||||
elemArr[index + 1] &&
|
|
||||||
elemArr[index + 1].nodeName === 'IMG' &&
|
|
||||||
elemArr[index + 1] &&
|
|
||||||
elemArr[index + 1].name !== 'emoji'
|
|
||||||
) {
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'text',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
text: filterXSS(res),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
formatInputCallBack && formatInputCallBack(data);
|
|
||||||
res = '';
|
|
||||||
}
|
|
||||||
} else if (child.nodeName === 'BR') {
|
|
||||||
res += '<br/>';
|
|
||||||
} else if (child.nodeName === 'IMG') {
|
|
||||||
// 有emjio 和 一般图片
|
|
||||||
// 图片解析后直接发送,不跟文字表情一组
|
|
||||||
if (child.name !== 'emoji') {
|
|
||||||
let srcReg = /src=[\'\']?([^\'\']*)[\'\']?/i;
|
|
||||||
let src = child.outerHTML.match(srcReg);
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'image',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
url: src[1],
|
|
||||||
path: src[1].replace(/http:\/\/[^\/]*/, ''),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
formatInputCallBack && formatInputCallBack(data);
|
|
||||||
} else {
|
|
||||||
// 非表情图片跟文字一起发送
|
|
||||||
res += child.outerHTML;
|
|
||||||
}
|
|
||||||
} else if (child.nodeName === 'DIV') {
|
|
||||||
res += `<div style='width:200px; white-space: nowrap;'>${child.outerHTML}</div>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (res) {
|
|
||||||
const data = {
|
|
||||||
from: 'customer',
|
|
||||||
mode: 'text',
|
|
||||||
date: new Date().getTime(),
|
|
||||||
content: {
|
|
||||||
text: filterXSS(res),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
formatInputCallBack && formatInputCallBack(data);
|
|
||||||
}
|
|
||||||
unref(virtualNode).innerHTML = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态回调
|
|
||||||
* @param {*} res -接口返回数据
|
|
||||||
*/
|
|
||||||
const callBackNotice = (res) => {
|
|
||||||
ElNotification({
|
|
||||||
title: 'socket',
|
|
||||||
message: res.msg,
|
|
||||||
showClose: true,
|
|
||||||
type: res.error === 0 ? 'success' : 'warning',
|
|
||||||
duration: 1200,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化发送信息
|
|
||||||
* @param {Object} message
|
|
||||||
* @returns obj - 消息对象
|
|
||||||
*/
|
|
||||||
const formatInput = (message) => {
|
|
||||||
let obj = {};
|
|
||||||
switch (message.mode) {
|
|
||||||
case 'text':
|
|
||||||
obj = {
|
|
||||||
message_type: 'text',
|
|
||||||
message: message.content.text,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
obj = {
|
|
||||||
message_type: 'image',
|
|
||||||
message: message.content.path,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'goods':
|
|
||||||
obj = {
|
|
||||||
message_type: 'goods',
|
|
||||||
message: message.content.item,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'order':
|
|
||||||
obj = {
|
|
||||||
message_type: 'order',
|
|
||||||
message: message.content.item,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 格式化接收信息
|
|
||||||
* @param {*} message
|
|
||||||
* @returns obj - 消息对象
|
|
||||||
*/
|
|
||||||
const formatMessage = (message) => {
|
|
||||||
let obj = {};
|
|
||||||
switch (message.message_type) {
|
|
||||||
case 'system':
|
|
||||||
obj = {
|
|
||||||
from: 'system', // 用户customer左 | 顾客customer_service右 | 系统system中间
|
|
||||||
mode: 'system', // goods,order,image,text,system
|
|
||||||
date: message.create_time * 1000, //时间
|
|
||||||
content: {
|
|
||||||
//内容
|
|
||||||
text: message.message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
obj = {
|
|
||||||
from: message.sender_identify,
|
|
||||||
mode: message.message_type,
|
|
||||||
date: message.create_time * 1000, //时间
|
|
||||||
sender: message.sender,
|
|
||||||
content: {
|
|
||||||
text: message.message,
|
|
||||||
messageId: message.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
obj = {
|
|
||||||
from: message.sender_identify,
|
|
||||||
mode: message.message_type,
|
|
||||||
date: message.create_time * 1000, //时间
|
|
||||||
sender: message.sender,
|
|
||||||
content: {
|
|
||||||
url: sheep.$url.cdn(message.message),
|
|
||||||
messageId: message.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'goods':
|
|
||||||
obj = {
|
|
||||||
from: message.sender_identify,
|
|
||||||
mode: message.message_type,
|
|
||||||
date: message.create_time * 1000, //时间
|
|
||||||
sender: message.sender,
|
|
||||||
content: {
|
|
||||||
item: message.message,
|
|
||||||
messageId: message.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case 'order':
|
|
||||||
obj = {
|
|
||||||
from: message.sender_identify,
|
|
||||||
mode: message.message_type,
|
|
||||||
date: message.create_time * 1000, //时间
|
|
||||||
sender: message.sender,
|
|
||||||
content: {
|
|
||||||
item: message.message,
|
|
||||||
messageId: message.id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* file 转换为 img
|
|
||||||
* @param {*} file - file 文件
|
|
||||||
* @returns img - img标签
|
|
||||||
*/
|
|
||||||
const readImg = (file) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
img.src = e.target.result;
|
|
||||||
};
|
|
||||||
reader.onerror = function (e) {
|
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
img.onload = function () {
|
|
||||||
resolve(img);
|
|
||||||
};
|
|
||||||
img.onerror = function (e) {
|
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 压缩图片
|
|
||||||
*@param img -被压缩的img对象
|
|
||||||
* @param type -压缩后转换的文件类型
|
|
||||||
* @param mx -触发压缩的图片最大宽度限制
|
|
||||||
* @param mh -触发压缩的图片最大高度限制
|
|
||||||
* @returns blob - 文件流
|
|
||||||
*/
|
|
||||||
const compressImg = (img, type = 'image/jpeg', mx = 1000, mh = 1000, quality = 1) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
const { width: originWidth, height: originHeight } = img;
|
|
||||||
// 最大尺寸限制
|
|
||||||
const maxWidth = mx;
|
|
||||||
const maxHeight = mh;
|
|
||||||
// 目标尺寸
|
|
||||||
let targetWidth = originWidth;
|
|
||||||
let targetHeight = originHeight;
|
|
||||||
if (originWidth > maxWidth || originHeight > maxHeight) {
|
|
||||||
if (originWidth / originHeight > 1) {
|
|
||||||
// 宽图片
|
|
||||||
targetWidth = maxWidth;
|
|
||||||
targetHeight = Math.round(maxWidth * (originHeight / originWidth));
|
|
||||||
} else {
|
|
||||||
// 高图片
|
|
||||||
targetHeight = maxHeight;
|
|
||||||
targetWidth = Math.round(maxHeight * (originWidth / originHeight));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvas.width = targetWidth;
|
|
||||||
canvas.height = targetHeight;
|
|
||||||
context.clearRect(0, 0, targetWidth, targetHeight);
|
|
||||||
// 图片绘制
|
|
||||||
context.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
||||||
canvas.toBlob(
|
|
||||||
function (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
},
|
|
||||||
type,
|
|
||||||
quality,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
compressImg,
|
|
||||||
readImg,
|
|
||||||
formatMessage,
|
|
||||||
formatInput,
|
|
||||||
callBackNotice,
|
|
||||||
|
|
||||||
socketInit,
|
|
||||||
socketSendMsg,
|
|
||||||
socketClose,
|
|
||||||
socketHistoryList,
|
|
||||||
|
|
||||||
getFocus,
|
|
||||||
formatChatInput,
|
|
||||||
onDrop,
|
|
||||||
onPaste,
|
|
||||||
upload,
|
|
||||||
state,
|
|
||||||
|
|
||||||
socketTest,
|
|
||||||
|
|
||||||
showTime,
|
|
||||||
formatTime,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import request from '@/sheep/request';
|
import request from '@/sheep/request';
|
||||||
|
|
||||||
const KeFuApi = {
|
const KeFuApi = {
|
||||||
sendMessage: (data) => {
|
sendKefuMessage: (data) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/promotion/kefu-message/send',
|
url: '/promotion/kefu-message/send',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -15,7 +15,7 @@ const KeFuApi = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getMessageListPage: (params) => {
|
getKefuMessagePage: (params) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/promotion/kefu-message/page',
|
url: '/promotion/kefu-message/page',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
Loading…
Reference in New Issue