!474 【优化】spu:新增商品属性属性值为空校验。【新增】mall 客服消息下拉加载,有新消息提醒

Merge pull request !474 from puhui999/dev-crm
pull/476/MERGE
芋道源码 2024-07-08 16:08:33 +00:00 committed by Gitee
commit 5162191340
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
5 changed files with 147 additions and 60 deletions

View File

@ -292,6 +292,7 @@ import { createImageViewer } from '@/components/ImageViewer'
import { RuleConfig } from '@/views/mall/product/spu/components/index' import { RuleConfig } from '@/views/mall/product/spu/components/index'
import { PropertyAndValues } from './index' import { PropertyAndValues } from './index'
import { ElTable } from 'element-plus' import { ElTable } from 'element-plus'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'SkuList' }) defineOptions({ name: 'SkuList' })
const message = useMessage() // const message = useMessage() //
@ -340,11 +341,22 @@ const imagePreview = (imgUrl: string) => {
/** 批量添加 */ /** 批量添加 */
const batchAdd = () => { const batchAdd = () => {
validateProperty()
formData.value!.skus!.forEach((item) => { formData.value!.skus!.forEach((item) => {
copyValueToTarget(item, skuList.value[0]) 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 */ /** 删除 sku */
const deleteSku = (row) => { const deleteSku = (row) => {
const index = formData.value!.skus!.findIndex( const index = formData.value!.skus!.findIndex(
@ -358,6 +370,7 @@ const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表
* 保存时每个商品规格的表单要校验下例如说销售金额最低是 0.01 这种 * 保存时每个商品规格的表单要校验下例如说销售金额最低是 0.01 这种
*/ */
const validateSku = () => { const validateSku = () => {
validateProperty()
let warningInfo = '请检查商品各行相关属性配置,' let warningInfo = '请检查商品各行相关属性配置,'
let validate = true // let validate = true //
for (const sku of formData.value!.skus!) { for (const sku of formData.value!.skus!) {
@ -421,7 +434,7 @@ watch(
const generateTableData = (propertyList: any[]) => { const generateTableData = (propertyList: any[]) => {
// //
const propertyValues = propertyList.map((item) => const propertyValues = propertyList.map((item) =>
item.values.map((v) => ({ item.values.map((v: any) => ({
propertyId: item.id, propertyId: item.id,
propertyName: item.name, propertyName: item.name,
valueId: v.id, valueId: v.id,
@ -464,15 +477,14 @@ const generateTableData = (propertyList: any[]) => {
*/ */
const validateData = (propertyList: any[]) => { const validateData = (propertyList: any[]) => {
const skuPropertyIds: number[] = [] const skuPropertyIds: number[] = []
formData.value!.skus!.forEach( formData.value!.skus!.forEach((sku) =>
(sku) => sku.properties
sku.properties ?.map((property) => property.propertyId)
?.map((property) => property.propertyId) ?.forEach((propertyId) => {
?.forEach((propertyId) => { if (skuPropertyIds.indexOf(propertyId!) === -1) {
if (skuPropertyIds.indexOf(propertyId!) === -1) { skuPropertyIds.push(propertyId!)
skuPropertyIds.push(propertyId!) }
} })
})
) )
const propertyIds = propertyList.map((item) => item.id) const propertyIds = propertyList.map((item) => item.id)
return skuPropertyIds.length === propertyIds.length return skuPropertyIds.length === propertyIds.length
@ -543,7 +555,7 @@ watch(
return return
} }
// //
if (propertyList.some((item) => item.values!.length === 0)) { if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
return return
} }
// table sku // table sku

View File

@ -3,7 +3,7 @@
<el-col v-for="(item, index) in attributeList" :key="index"> <el-col v-for="(item, index) in attributeList" :key="index">
<div> <div>
<el-text class="mx-1">属性名</el-text> <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 }} {{ item.name }}
</el-tag> </el-tag>
</div> </div>
@ -12,8 +12,8 @@
<el-tag <el-tag
v-for="(value, valueIndex) in item.values" v-for="(value, valueIndex) in item.values"
:key="value.id" :key="value.id"
class="mx-1"
:closable="!isDetail" :closable="!isDetail"
class="mx-1"
@close="handleCloseValue(index, valueIndex)" @close="handleCloseValue(index, valueIndex)"
> >
{{ value.name }} {{ value.name }}
@ -44,7 +44,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElInput } from 'element-plus' import { ElInput } from 'element-plus'
import * as PropertyApi from '@/api/mall/product/property' import * as PropertyApi from '@/api/mall/product/property'
import { PropertyVO } from '@/api/mall/product/property'
import { PropertyAndValues } from '@/views/mall/product/spu/components' import { PropertyAndValues } from '@/views/mall/product/spu/components'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
@ -59,9 +58,9 @@ const inputVisible = computed(() => (index: number) => {
if (attributeIndex.value === null) return false if (attributeIndex.value === null) return false
if (attributeIndex.value === index) return true if (attributeIndex.value === index) return true
}) })
const inputRef = ref([]) //Ref const inputRef = ref<any[]>([]) //Ref
/** 解决 ref 在 v-for 中的获取问题*/ /** 解决 ref 在 v-for 中的获取问题*/
const setInputRef = (el) => { const setInputRef = (el: any) => {
if (el === null || typeof el === 'undefined') return if (el === null || typeof el === 'undefined') return
// id // id
if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) { if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
@ -81,7 +80,7 @@ watch(
() => props.propertyList, () => props.propertyList,
(data) => { (data) => {
if (!data) return if (!data) return
attributeList.value = data attributeList.value = data as any
}, },
{ {
deep: true, deep: true,
@ -97,6 +96,7 @@ const handleCloseValue = (index: number, valueIndex: number) => {
/** 删除属性*/ /** 删除属性*/
const handleCloseProperty = (index: number) => { const handleCloseProperty = (index: number) => {
attributeList.value?.splice(index, 1) attributeList.value?.splice(index, 1)
emit('success', attributeList.value)
} }
/** 显示输入框并获取焦点 */ /** 显示输入框并获取焦点 */

View File

@ -1,18 +1,18 @@
<!-- 商品发布 - 库存价格 --> <!-- 商品发布 - 库存价格 -->
<template> <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-form-item label="分销类型" props="subCommissionType">
<el-radio-group <el-radio-group
v-model="formData.subCommissionType" v-model="formData.subCommissionType"
@change="changeSubCommissionType"
class="w-80" class="w-80"
@change="changeSubCommissionType"
> >
<el-radio :label="false">默认设置</el-radio> <el-radio :label="false">默认设置</el-radio>
<el-radio :label="true" class="radio">单独设置</el-radio> <el-radio :label="true" class="radio">单独设置</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="商品规格" props="specType"> <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="false" class="radio">单规格</el-radio>
<el-radio :label="true">多规格</el-radio> <el-radio :label="true">多规格</el-radio>
</el-radio-group> </el-radio-group>
@ -29,22 +29,22 @@
<el-form-item v-if="formData.specType" label="商品属性"> <el-form-item v-if="formData.specType" label="商品属性">
<el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open"></el-button> <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open"></el-button>
<ProductAttributes <ProductAttributes
:is-detail="isDetail"
:property-list="propertyList" :property-list="propertyList"
@success="generateSkus" @success="generateSkus"
:is-detail="isDetail"
/> />
</el-form-item> </el-form-item>
<template v-if="formData.specType && propertyList.length > 0"> <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" /> <SkuList :is-batch="true" :prop-form-data="formData" :property-list="propertyList" />
</el-form-item> </el-form-item>
<el-form-item label="规格列表"> <el-form-item label="规格列表">
<SkuList <SkuList
ref="skuListRef" ref="skuListRef"
:is-detail="isDetail"
:prop-form-data="formData" :prop-form-data="formData"
:property-list="propertyList" :property-list="propertyList"
:rule-config="ruleConfig" :rule-config="ruleConfig"
:is-detail="isDetail"
/> />
</el-form-item> </el-form-item>
</template> </template>
@ -181,7 +181,7 @@ const onChangeSpec = () => {
} }
/** 调用 SkuList generateTableData 方法*/ /** 调用 SkuList generateTableData 方法*/
const generateSkus = (propertyList) => { const generateSkus = (propertyList: any[]) => {
skuListRef.value.generateTableData(propertyList) skuListRef.value.generateTableData(propertyList)
} }
</script> </script>

View File

@ -4,9 +4,16 @@
<div class="kefu-title">{{ keFuConversation.userNickname }}</div> <div class="kefu-title">{{ keFuConversation.userNickname }}</div>
</el-header> </el-header>
<el-main class="kefu-content" style="overflow: visible"> <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 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 class="flex justify-center items-center mb-20px">
<!-- 日期 --> <!-- 日期 -->
<div <div
@ -58,6 +65,14 @@
</div> </div>
</div> </div>
</el-scrollbar> </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-main>
<el-footer height="230px"> <el-footer height="230px">
<div class="h-[100%]"> <div class="h-[100%]">
@ -101,23 +116,47 @@ const messageTool = useMessage()
const message = ref('') // const message = ref('') //
const messageList = ref<KeFuMessageRespVO[]>([]) // const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 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) => { const getMessageList = async (conversation: KeFuConversationRespVO) => {
keFuConversation.value = conversation keFuConversation.value = conversation
const { list } = await KeFuMessageApi.getKeFuMessagePage({ queryParams.conversationId = conversation.id
pageNo: 1, const messageTotal = messageList.value.length
conversationId: conversation.id if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
}) return
messageList.value = list.reverse() }
// TODO puhui999: 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() 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) { if (!keFuConversation.value) {
return return
} }
getMessageList(keFuConversation.value)
queryParams.pageNo = 1
await getMessageList(keFuConversation.value)
if (loadHistory.value) {
//
showNewMessageTip.value = true
}
} }
defineExpose({ getMessageList, refreshMessageList }) defineExpose({ getMessageList, refreshMessageList })
// //
@ -140,7 +179,7 @@ const handleSendPicture = async (picUrl: string) => {
const handleSendMessage = async () => { const handleSendMessage = async () => {
// 1. // 1.
if (isEmpty(unref(message.value))) { if (isEmpty(unref(message.value))) {
messageTool.warning('请输入消息后再发送哦!') messageTool.notifyWarning('请输入消息后再发送哦!')
return return
} }
// 2. // 2.
@ -167,12 +206,41 @@ const innerRef = ref<HTMLDivElement>()
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>() const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
// //
const scrollToBottom = async () => { const scrollToBottom = async () => {
// 1. // 1.
if (loadHistory.value) {
return
}
// 2.1
await nextTick() await nextTick()
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight) scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
// 2. showNewMessageTip.value = false
// 2.2
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id) 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 - 数据 * @param {*} item - 数据
@ -196,6 +264,32 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
} }
&-content { &-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 { .ss-row-left {
justify-content: flex-start; justify-content: flex-start;

View File

@ -74,7 +74,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation' import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { useEmoji } from './tools/emoji' import { useEmoji } from './tools/emoji'
import { formatDate, getNowDateTime } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from './tools/constants' import { KeFuMessageContentTypeEnum } from './tools/constants'
defineOptions({ name: 'KeFuConversationBox' }) defineOptions({ name: 'KeFuConversationBox' })
@ -84,24 +84,6 @@ const activeConversationIndex = ref(-1) // 选中的会话
const conversationList = ref<KeFuConversationRespVO[]>([]) // const conversationList = ref<KeFuConversationRespVO[]>([]) //
const getConversationList = async () => { const getConversationList = async () => {
conversationList.value = await KeFuConversationApi.getConversationList() 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 }) defineExpose({ getConversationList })
const emits = defineEmits<{ const emits = defineEmits<{
@ -157,8 +139,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
id: selectedConversation.value.id, id: selectedConversation.value.id,
adminPinned adminPinned
}) })
// TODO puhui999: message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
message.success(adminPinned ? '置顶成功' : '取消置顶成功')
// 2. // 2.
closeRightMenu() closeRightMenu()
await getConversationList() await getConversationList()