Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
51621913
authored
Jul 08, 2024
by
芋道源码
Committed by
Gitee
Jul 08, 2024
Browse files
Options
Browse Files
Download
Plain Diff
!474 【优化】spu:新增商品属性属性值为空校验。【新增】mall 客服消息下拉加载,有新消息提醒
Merge pull request !474 from puhui999/dev-crm
parents
82b53b9b
848bc606
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
147 additions
and
60 deletions
+147
-60
src/views/mall/product/spu/components/SkuList.vue
+24
-12
src/views/mall/product/spu/form/ProductAttributes.vue
+6
-6
src/views/mall/product/spu/form/SkuForm.vue
+7
-7
src/views/mall/promotion/kefu/components/KeFuChatBox.vue
+108
-14
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue
+2
-21
No files found.
src/views/mall/product/spu/components/SkuList.vue
View file @
51621913
...
...
@@ -292,6 +292,7 @@ import { createImageViewer } from '@/components/ImageViewer'
import
{
RuleConfig
}
from
'@/views/mall/product/spu/components/index'
import
{
PropertyAndValues
}
from
'./index'
import
{
ElTable
}
from
'element-plus'
import
{
isEmpty
}
from
'@/utils/is'
defineOptions
({
name
:
'SkuList'
})
const
message
=
useMessage
()
// 消息弹窗
...
...
@@ -340,11 +341,22 @@ const imagePreview = (imgUrl: string) => {
/** 批量添加 */
const
batchAdd
=
()
=>
{
validateProperty
()
formData
.
value
!
.
skus
!
.
forEach
((
item
)
=>
{
copyValueToTarget
(
item
,
skuList
.
value
[
0
])
})
}
/** 校验商品属性属性值 */
const
validateProperty
=
()
=>
{
// 校验商品属性属性值是否为空,有一个为空都不给过
const
warningInfo
=
'存在属性属性值为空,请先检查完善属性值后重试!!!'
for
(
const
item
of
props
.
propertyList
)
{
if
(
!
item
.
values
||
isEmpty
(
item
.
values
))
{
message
.
warning
(
warningInfo
)
throw
new
Error
(
warningInfo
)
}
}
}
/** 删除 sku */
const
deleteSku
=
(
row
)
=>
{
const
index
=
formData
.
value
!
.
skus
!
.
findIndex
(
...
...
@@ -358,6 +370,7 @@ const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
*/
const
validateSku
=
()
=>
{
validateProperty
()
let
warningInfo
=
'请检查商品各行相关属性配置,'
let
validate
=
true
// 默认通过
for
(
const
sku
of
formData
.
value
!
.
skus
!
)
{
...
...
@@ -421,7 +434,7 @@ watch(
const
generateTableData
=
(
propertyList
:
any
[])
=>
{
// 构建数据结构
const
propertyValues
=
propertyList
.
map
((
item
)
=>
item
.
values
.
map
((
v
)
=>
({
item
.
values
.
map
((
v
:
any
)
=>
({
propertyId
:
item
.
id
,
propertyName
:
item
.
name
,
valueId
:
v
.
id
,
...
...
@@ -464,15 +477,14 @@ const generateTableData = (propertyList: any[]) => {
*/
const
validateData
=
(
propertyList
:
any
[])
=>
{
const
skuPropertyIds
:
number
[]
=
[]
formData
.
value
!
.
skus
!
.
forEach
(
(
sku
)
=>
sku
.
properties
?.
map
((
property
)
=>
property
.
propertyId
)
?.
forEach
((
propertyId
)
=>
{
if
(
skuPropertyIds
.
indexOf
(
propertyId
!
)
===
-
1
)
{
skuPropertyIds
.
push
(
propertyId
!
)
}
})
formData
.
value
!
.
skus
!
.
forEach
((
sku
)
=>
sku
.
properties
?.
map
((
property
)
=>
property
.
propertyId
)
?.
forEach
((
propertyId
)
=>
{
if
(
skuPropertyIds
.
indexOf
(
propertyId
!
)
===
-
1
)
{
skuPropertyIds
.
push
(
propertyId
!
)
}
})
)
const
propertyIds
=
propertyList
.
map
((
item
)
=>
item
.
id
)
return
skuPropertyIds
.
length
===
propertyIds
.
length
...
...
@@ -543,7 +555,7 @@ watch(
return
}
// 添加新属性没有属性值也不做处理
if
(
propertyList
.
some
((
item
)
=>
item
.
values
!
.
length
===
0
))
{
if
(
propertyList
.
some
((
item
)
=>
!
item
.
values
||
isEmpty
(
item
.
values
)
))
{
return
}
// 生成 table 数据,即 sku 列表
...
...
src/views/mall/product/spu/form/ProductAttributes.vue
View file @
51621913
...
...
@@ -3,7 +3,7 @@
<el-col
v-for=
"(item, index) in attributeList"
:key=
"index"
>
<div>
<el-text
class=
"mx-1"
>
属性名:
</el-text>
<el-tag
class=
"mx-1"
:closable=
"!isDetail
"
type=
"success"
@
close=
"handleCloseProperty(index)"
>
<el-tag
:closable=
"!isDetail"
class=
"mx-1
"
type=
"success"
@
close=
"handleCloseProperty(index)"
>
{{
item
.
name
}}
</el-tag>
</div>
...
...
@@ -12,8 +12,8 @@
<el-tag
v-for=
"(value, valueIndex) in item.values"
:key=
"value.id"
class=
"mx-1"
:closable=
"!isDetail"
class=
"mx-1"
@
close=
"handleCloseValue(index, valueIndex)"
>
{{
value
.
name
}}
...
...
@@ -44,7 +44,6 @@
<
script
lang=
"ts"
setup
>
import
{
ElInput
}
from
'element-plus'
import
*
as
PropertyApi
from
'@/api/mall/product/property'
import
{
PropertyVO
}
from
'@/api/mall/product/property'
import
{
PropertyAndValues
}
from
'@/views/mall/product/spu/components'
import
{
propTypes
}
from
'@/utils/propTypes'
...
...
@@ -59,9 +58,9 @@ const inputVisible = computed(() => (index: number) => {
if
(
attributeIndex
.
value
===
null
)
return
false
if
(
attributeIndex
.
value
===
index
)
return
true
})
const
inputRef
=
ref
([])
//标签输入框Ref
const
inputRef
=
ref
<
any
[]
>
([])
//标签输入框Ref
/** 解决 ref 在 v-for 中的获取问题*/
const
setInputRef
=
(
el
)
=>
{
const
setInputRef
=
(
el
:
any
)
=>
{
if
(
el
===
null
||
typeof
el
===
'undefined'
)
return
// 如果不存在 id 相同的元素才添加
if
(
!
inputRef
.
value
.
some
((
item
)
=>
item
.
input
?.
attributes
.
id
===
el
.
input
?.
attributes
.
id
))
{
...
...
@@ -81,7 +80,7 @@ watch(
()
=>
props
.
propertyList
,
(
data
)
=>
{
if
(
!
data
)
return
attributeList
.
value
=
data
attributeList
.
value
=
data
as
any
},
{
deep
:
true
,
...
...
@@ -97,6 +96,7 @@ const handleCloseValue = (index: number, valueIndex: number) => {
/** 删除属性*/
const
handleCloseProperty
=
(
index
:
number
)
=>
{
attributeList
.
value
?.
splice
(
index
,
1
)
emit
(
'success'
,
attributeList
.
value
)
}
/** 显示输入框并获取焦点 */
...
...
src/views/mall/product/spu/form/SkuForm.vue
View file @
51621913
<!-- 商品发布 - 库存价格 -->
<
template
>
<el-form
ref=
"formRef"
:
model=
"formData"
:rules=
"rules"
label-width=
"120px"
:disabled=
"isDetail
"
>
<el-form
ref=
"formRef"
:
disabled=
"isDetail"
:model=
"formData"
:rules=
"rules"
label-width=
"120px
"
>
<el-form-item
label=
"分销类型"
props=
"subCommissionType"
>
<el-radio-group
v-model=
"formData.subCommissionType"
@
change=
"changeSubCommissionType"
class=
"w-80"
@
change=
"changeSubCommissionType"
>
<el-radio
:label=
"false"
>
默认设置
</el-radio>
<el-radio
:label=
"true"
class=
"radio"
>
单独设置
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label=
"商品规格"
props=
"specType"
>
<el-radio-group
v-model=
"formData.specType"
@
change=
"onChangeSpec"
class=
"w-80
"
>
<el-radio-group
v-model=
"formData.specType"
class=
"w-80"
@
change=
"onChangeSpec
"
>
<el-radio
:label=
"false"
class=
"radio"
>
单规格
</el-radio>
<el-radio
:label=
"true"
>
多规格
</el-radio>
</el-radio-group>
...
...
@@ -29,22 +29,22 @@
<el-form-item
v-if=
"formData.specType"
label=
"商品属性"
>
<el-button
class=
"mb-10px mr-15px"
@
click=
"attributesAddFormRef.open"
>
添加属性
</el-button>
<ProductAttributes
:is-detail=
"isDetail"
:property-list=
"propertyList"
@
success=
"generateSkus"
:is-detail=
"isDetail"
/>
</el-form-item>
<template
v-if=
"formData.specType && propertyList.length > 0"
>
<el-form-item
label=
"批量设置"
v-if=
"!isDetail
"
>
<el-form-item
v-if=
"!isDetail"
label=
"批量设置
"
>
<SkuList
:is-batch=
"true"
:prop-form-data=
"formData"
:property-list=
"propertyList"
/>
</el-form-item>
<el-form-item
label=
"规格列表"
>
<SkuList
ref=
"skuListRef"
:is-detail=
"isDetail"
:prop-form-data=
"formData"
:property-list=
"propertyList"
:rule-config=
"ruleConfig"
:is-detail=
"isDetail"
/>
</el-form-item>
</
template
>
...
...
@@ -181,7 +181,7 @@ const onChangeSpec = () => {
}
/** 调用 SkuList generateTableData 方法*/
const
generateSkus
=
(
propertyList
)
=>
{
const
generateSkus
=
(
propertyList
:
any
[]
)
=>
{
skuListRef
.
value
.
generateTableData
(
propertyList
)
}
</
script
>
src/views/mall/promotion/kefu/components/KeFuChatBox.vue
View file @
51621913
...
...
@@ -4,9 +4,16 @@
<div
class=
"kefu-title"
>
{{
keFuConversation
.
userNickname
}}
</div>
</el-header>
<el-main
class=
"kefu-content"
style=
"overflow: visible"
>
<el-scrollbar
ref=
"scrollbarRef"
always
height=
"calc(100vh - 495px)"
>
<div
v-show=
"loadingMore"
class=
"loadingMore flex justify-center items-center cursor-pointer"
@
click=
"handleOldMessage"
>
加载更多
</div>
<el-scrollbar
ref=
"scrollbarRef"
always
height=
"calc(100vh - 495px)"
@
scroll=
"handleScroll"
>
<div
ref=
"innerRef"
class=
"w-[100%] pb-3px"
>
<div
v-for=
"(item, index) in
messageList
"
:key=
"item.id"
class=
"w-[100%]"
>
<div
v-for=
"(item, index) in
getMessageList0
"
:key=
"item.id"
class=
"w-[100%]"
>
<div
class=
"flex justify-center items-center mb-20px"
>
<!-- 日期 -->
<div
...
...
@@ -58,6 +65,14 @@
</div>
</div>
</el-scrollbar>
<div
v-show=
"showNewMessageTip"
class=
"newMessageTip flex items-center cursor-pointer"
@
click=
"handleToNewMessage"
>
<span>
有新消息
</span>
<Icon
class=
"ml-5px"
icon=
"ep:bottom"
/>
</div>
</el-main>
<el-footer
height=
"230px"
>
<div
class=
"h-[100%]"
>
...
...
@@ -101,23 +116,47 @@ const messageTool = useMessage()
const
message
=
ref
(
''
)
// 消息
const
messageList
=
ref
<
KeFuMessageRespVO
[]
>
([])
// 消息列表
const
keFuConversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
// 获得消息 TODO puhui999: 先不考虑下拉加载历史消息
const
showNewMessageTip
=
ref
(
false
)
// 显示有新消息提示
const
queryParams
=
reactive
({
pageNo
:
1
,
conversationId
:
0
})
const
total
=
ref
(
0
)
// 消息总条数
// 获得消息
const
getMessageList
=
async
(
conversation
:
KeFuConversationRespVO
)
=>
{
keFuConversation
.
value
=
conversation
const
{
list
}
=
await
KeFuMessageApi
.
getKeFuMessagePage
({
pageNo
:
1
,
conversationId
:
conversation
.
id
})
messageList
.
value
=
list
.
reverse
()
// TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
queryParams
.
conversationId
=
conversation
.
id
const
messageTotal
=
messageList
.
value
.
length
if
(
total
.
value
>
0
&&
messageTotal
>
0
&&
messageTotal
===
total
.
value
)
{
return
}
const
res
=
await
KeFuMessageApi
.
getKeFuMessagePage
(
queryParams
)
total
.
value
=
res
.
total
for
(
const
item
of
res
.
list
)
{
if
(
messageList
.
value
.
some
((
val
)
=>
val
.
id
===
item
.
id
))
{
continue
}
messageList
.
value
.
push
(
item
)
}
await
scrollToBottom
()
}
const
getMessageList0
=
computed
(()
=>
{
messageList
.
value
.
sort
((
a
:
any
,
b
:
any
)
=>
a
.
createTime
-
b
.
createTime
)
return
messageList
.
value
})
// 刷新消息列表
const
refreshMessageList
=
()
=>
{
const
refreshMessageList
=
async
()
=>
{
if
(
!
keFuConversation
.
value
)
{
return
}
getMessageList
(
keFuConversation
.
value
)
queryParams
.
pageNo
=
1
await
getMessageList
(
keFuConversation
.
value
)
if
(
loadHistory
.
value
)
{
// 有下角显示有新消息提示
showNewMessageTip
.
value
=
true
}
}
defineExpose
({
getMessageList
,
refreshMessageList
})
// 是否显示聊天区域
...
...
@@ -140,7 +179,7 @@ const handleSendPicture = async (picUrl: string) => {
const
handleSendMessage
=
async
()
=>
{
// 1. 校验消息是否为空
if
(
isEmpty
(
unref
(
message
.
value
)))
{
messageTool
.
w
arning
(
'请输入消息后再发送哦!'
)
messageTool
.
notifyW
arning
(
'请输入消息后再发送哦!'
)
return
}
// 2. 组织发送消息
...
...
@@ -167,12 +206,41 @@ const innerRef = ref<HTMLDivElement>()
const
scrollbarRef
=
ref
<
InstanceType
<
typeof
ElScrollbarType
>>
()
// 滚动到底部
const
scrollToBottom
=
async
()
=>
{
// 1. 滚动到最新消息
// 1. 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
if
(
loadHistory
.
value
)
{
return
}
// 2.1 滚动到最新消息,关闭新消息提示
await
nextTick
()
scrollbarRef
.
value
!
.
setScrollTop
(
innerRef
.
value
!
.
clientHeight
)
// 2. 消息已读
showNewMessageTip
.
value
=
false
// 2.2 消息已读
await
KeFuMessageApi
.
updateKeFuMessageReadStatus
(
keFuConversation
.
value
.
id
)
}
// 查看新消息
const
handleToNewMessage
=
async
()
=>
{
loadHistory
.
value
=
false
await
scrollToBottom
()
}
const
loadingMore
=
ref
(
false
)
// 滚动到顶部加载更多
const
loadHistory
=
ref
(
false
)
// 加载历史消息
const
handleScroll
=
async
({
scrollTop
})
=>
{
const
messageTotal
=
messageList
.
value
.
length
if
(
total
.
value
>
0
&&
messageTotal
>
0
&&
messageTotal
===
total
.
value
)
{
return
}
// 距顶 20 加载下一页数据
loadingMore
.
value
=
scrollTop
<
20
}
const
handleOldMessage
=
async
()
=>
{
loadHistory
.
value
=
true
// 加载消息列表
queryParams
.
pageNo
+=
1
await
getMessageList
(
keFuConversation
.
value
)
loadingMore
.
value
=
false
// TODO puhui999: 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
}
/**
* 是否显示时间
* @param {*} item - 数据
...
...
@@ -196,6 +264,32 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
}
&
-content
{
position
:
relative
;
.loadingMore
{
position
:
absolute
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
50px
;
background-color
:
#eee
;
color
:
#666
;
text-align
:
center
;
line-height
:
50px
;
transform
:
translateY
(
-100%
);
transition
:
transform
0.3s
ease-in-out
;
}
.newMessageTip
{
position
:
absolute
;
bottom
:
35px
;
right
:
35px
;
background-color
:
#fff
;
padding
:
10px
;
border-radius
:
30px
;
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
/* 阴影效果 */
}
.ss-row-left
{
justify-content
:
flex-start
;
...
...
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue
View file @
51621913
...
...
@@ -74,7 +74,7 @@
<
script
lang=
"ts"
setup
>
import
{
KeFuConversationApi
,
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
useEmoji
}
from
'./tools/emoji'
import
{
formatDate
,
getNowDateTime
}
from
'@/utils/formatTime'
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
KeFuMessageContentTypeEnum
}
from
'./tools/constants'
defineOptions
({
name
:
'KeFuConversationBox'
})
...
...
@@ -84,24 +84,6 @@ const activeConversationIndex = ref(-1) // 选中的会话
const
conversationList
=
ref
<
KeFuConversationRespVO
[]
>
([])
// 会话列表
const
getConversationList
=
async
()
=>
{
conversationList
.
value
=
await
KeFuConversationApi
.
getConversationList
()
// 测试数据
for
(
let
i
=
0
;
i
<
5
;
i
++
)
{
conversationList
.
value
.
push
({
id
:
1
,
userId
:
283
,
userAvatar
:
'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132'
,
userNickname
:
'辉辉鸭'
+
i
,
lastMessageTime
:
getNowDateTime
(),
lastMessageContent
:
'[爱心][爱心]你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇'
,
lastMessageContentType
:
1
,
adminPinned
:
false
,
userDeleted
:
false
,
adminDeleted
:
false
,
adminUnreadMessageCount
:
i
})
}
}
defineExpose
({
getConversationList
})
const
emits
=
defineEmits
<
{
...
...
@@ -157,8 +139,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
id
:
selectedConversation
.
value
.
id
,
adminPinned
})
// TODO puhui999: 快速操作两次提示只会提示一次看看怎么优雅解决
message
.
success
(
adminPinned
?
'置顶成功'
:
'取消置顶成功'
)
message
.
notifySuccess
(
adminPinned
?
'置顶成功'
:
'取消置顶成功'
)
// 2. 关闭右键菜单,更新会话列表
closeRightMenu
()
await
getConversationList
()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment