!474 【优化】spu:新增商品属性属性值为空校验。【新增】mall 客服消息下拉加载,有新消息提醒
Merge pull request !474 from puhui999/dev-crmpull/476/MERGE
commit
5162191340
|
@ -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 列表
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/** 显示输入框并获取焦点 */
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<!-- 商品发布 - 库存价格 -->
|
||||
<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>
|
||||
|
|
|
@ -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.warning('请输入消息后再发送哦!')
|
||||
messageTool.notifyWarning('请输入消息后再发送哦!')
|
||||
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;
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue