From 917b9d180a01dc36aa3609fde8dd79b78bde8b68 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Sat, 22 Apr 2023 20:45:03 +0800 Subject: [PATCH 01/28] =?UTF-8?q?fix:=20mp/menu=E8=8F=9C=E5=8D=95=E6=8B=96?= =?UTF-8?q?=E5=8A=A8=E5=90=8E=E6=BF=80=E6=B4=BB=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mp/menu/components/MenuPreviewer.vue | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/views/mp/menu/components/MenuPreviewer.vue b/src/views/mp/menu/components/MenuPreviewer.vue index 3066e590..d2626320 100644 --- a/src/views/mp/menu/components/MenuPreviewer.vue +++ b/src/views/mp/menu/components/MenuPreviewer.vue @@ -4,7 +4,7 @@ item-key="id" ghost-class="draggable-ghost" :animation="400" - @end="onDragEnd" + @end="onParentDragEnd" > <template #item="{ element: parent, index: x }"> <div class="menu_bottom"> @@ -23,6 +23,7 @@ item-key="id" ghost-class="draggable-ghost" :animation="400" + @end="onChildDragEnd" > <template #item="{ element: child, index: y }"> <div class="subtitle menu_bottom"> @@ -118,42 +119,49 @@ const subMenuClicked = (child: Menu, x: number, y: number) => { } /** - * 处理一级菜单展开后被拖动 + * 处理一级菜单展开后被拖动,激活(展开)原来活动的一级菜单 * * @param oldIndex: 一级菜单拖动前的位置 * @param newIndex: 一级菜单拖动后的位置 */ -const onDragEnd = ({ oldIndex, newIndex }) => { +const onParentDragEnd = ({ oldIndex, newIndex }) => { // 二级菜单没有展开,直接返回 if (props.activeIndex === '__MENU_NOT_SELECTED__') { return } - let newParent = props.parentIndex - if (props.parentIndex === oldIndex) { - newParent = newIndex - } else if (props.parentIndex === newIndex) { - newParent = oldIndex - } else { - // 如果展开的二级菜单下标`props.parentIndex`不是被移动的菜单的前后下标。 - // 那么使用一个辅助素组来模拟菜单移动,然后找到展开的二级菜单的新下标`newParent` - let positions = new Array<boolean>(menuList.value.length).fill(false) - positions[props.parentIndex] = true - positions.splice(oldIndex, 1) - positions.splice(newIndex, 0, true) - newParent = positions.indexOf(true) - } + // 使用一个辅助数组来模拟菜单移动,然后找到展开的二级菜单的新下标`newParent` + let positions = new Array<boolean>(menuList.value.length).fill(false) + positions[props.parentIndex] = true + const [out] = positions.splice(oldIndex, 1) // 移出菜单,保存到变量out + positions.splice(newIndex, 0, out) // 把out变量插入被移出的菜单 + const newParentIndex = positions.indexOf(true) // 找到菜单元素,触发一级菜单点击 - const parent = menuList.value[newParent] - emit('menu-clicked', parent, newParent) + const parent = menuList.value[newParentIndex] + emit('menu-clicked', parent, newParentIndex) +} + +/** + * 处理二级菜单展开后被拖动,激活被拖动的菜单 + * + * @param newIndex 二级菜单拖动后的位置 + */ +const onChildDragEnd = ({ newIndex }) => { + const x = props.parentIndex + const y = newIndex + const children = menuList.value[x]?.children + if (children && children?.length > 0) { + const child = children[y] + emit('submenu-clicked', child, x, y) + } } </script> <style lang="scss" scoped> .menu_bottom { position: relative; - display: inline-block; + display: block; float: left; width: 85.5px; text-align: center; From f848f3ba91a2d4b09e8acbd7b47289055527c870 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Sat, 22 Apr 2023 20:51:25 +0800 Subject: [PATCH 02/28] =?UTF-8?q?refactor:=20mp/wx-msg=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/wx-msg/components/MsgEvent.vue | 51 ++++ .../components/wx-msg/components/MsgList.vue | 110 +++++++++ src/views/mp/components/wx-msg/main.vue | 219 ++++-------------- src/views/mp/components/wx-msg/types.ts | 6 + 4 files changed, 207 insertions(+), 179 deletions(-) create mode 100644 src/views/mp/components/wx-msg/components/MsgEvent.vue create mode 100644 src/views/mp/components/wx-msg/components/MsgList.vue diff --git a/src/views/mp/components/wx-msg/components/MsgEvent.vue b/src/views/mp/components/wx-msg/components/MsgEvent.vue new file mode 100644 index 00000000..e13e3112 --- /dev/null +++ b/src/views/mp/components/wx-msg/components/MsgEvent.vue @@ -0,0 +1,51 @@ +<template> + <div> + <div v-if="item.event === 'subscribe'"> + <el-tag type="success">关注</el-tag> + </div> + <div v-else-if="item.event === 'unsubscribe'"> + <el-tag type="danger">取消关注</el-tag> + </div> + <div v-else-if="item.event === 'CLICK'"> + <el-tag>点击菜单</el-tag> + 【{{ item.eventKey }}】 + </div> + <div v-else-if="item.event === 'VIEW'"> + <el-tag>点击菜单链接</el-tag> + 【{{ item.eventKey }}】 + </div> + <div v-else-if="item.event === 'scancode_waitmsg'"> + <el-tag>扫码结果</el-tag> + 【{{ item.eventKey }}】 + </div> + <div v-else-if="item.event === 'scancode_push'"> + <el-tag>扫码结果</el-tag> + 【{{ item.eventKey }}】 + </div> + <div v-else-if="item.event === 'pic_sysphoto'"> + <el-tag>系统拍照发图</el-tag> + </div> + <div v-else-if="item.event === 'pic_photo_or_album'"> + <el-tag>拍照或者相册</el-tag> + </div> + <div v-else-if="item.event === 'pic_weixin'"> + <el-tag>微信相册</el-tag> + </div> + <div v-else-if="item.event === 'location_select'"> + <el-tag>选择地理位置</el-tag> + </div> + <div v-else> + <el-tag type="danger">未知事件类型</el-tag> + </div> + </div> +</template> + +<script setup lang="ts"> +const props = defineProps<{ + item: any +}>() + +const item = ref(props.item) +</script> + +<style scoped></style> diff --git a/src/views/mp/components/wx-msg/components/MsgList.vue b/src/views/mp/components/wx-msg/components/MsgList.vue new file mode 100644 index 00000000..39f7203c --- /dev/null +++ b/src/views/mp/components/wx-msg/components/MsgList.vue @@ -0,0 +1,110 @@ +<template> + <div class="execution" v-for="item in props.list" :key="item.id"> + <div + class="avue-comment" + :class="{ 'avue-comment--reverse': item.sendFrom === SendFrom.MpBot }" + > + <div class="avatar-div"> + <img :src="getAvatar(item.sendFrom)" class="avue-comment__avatar" /> + <div class="avue-comment__author"> + {{ getNickname(item.sendFrom) }} + </div> + </div> + <div class="avue-comment__main"> + <div class="avue-comment__header"> + <div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div> + </div> + <div + class="avue-comment__body" + :style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''" + > + <!-- 【事件】区域 --> + <MsgEvent v-if="item.type === MsgType.Event" :item="item" /> + <!-- 【消息】区域 --> + <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> + <div v-else-if="item.type === MsgType.Voice"> + <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> + </div> + <div v-else-if="item.type === MsgType.Image"> + <a target="_blank" :href="item.mediaUrl"> + <img :src="item.mediaUrl" style="width: 100px" /> + </a> + </div> + <div + v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" + style="text-align: center" + > + <WxVideoPlayer :url="item.mediaUrl" /> + </div> + <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> + <el-link type="success" :underline="false" target="_blank" :href="item.url"> + <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> + </el-link> + <div class="avue-card__info" style="height: unset">{{ item.description }}</div> + </div> + <!-- TODO 芋艿:待完善 --> + <div v-else-if="item.type === MsgType.Location"> + <WxLocation + :label="item.label" + :location-y="item.locationY" + :location-x="item.locationX" + /> + </div> + <div v-else-if="item.type === MsgType.News" style="width: 300px"> + <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> + <WxNews :articles="item.articles" /> + </div> + <div v-else-if="item.type === MsgType.Music"> + <WxMusic + :title="item.title" + :description="item.description" + :thumb-media-url="item.thumbMediaUrl" + :music-url="item.musicUrl" + :hq-music-url="item.hqMusicUrl" + /> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts" name="MsgList"> +import WxVideoPlayer from '@/views/mp/components/wx-video-play' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play' +import WxNews from '@/views/mp/components/wx-news' +import WxLocation from '@/views/mp/components/wx-location' +import WxMusic from '@/views/mp/components/wx-music' +import MsgEvent from './MsgEvent.vue' +import { formatDate } from '@/utils/formatTime' +import { MsgType, User } from '../types' +import avatarWechat from '@/assets/imgs/wechat.png' + +const props = defineProps<{ + list: any[] + accountId: number + user: User +}>() + +enum SendFrom { + User = 1, + MpBot = 2 +} + +const getAvatar = (sendFrom: SendFrom) => + sendFrom === SendFrom.User ? props.user.avatar : avatarWechat + +const getNickname = (sendFrom: SendFrom) => + sendFrom === SendFrom.User ? props.user.nickname : '公众号' +</script> + +<style lang="scss" scoped> +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ +@import '../comment.scss'; +@import '../card.scss'; + +.avatar-div { + text-align: center; + width: 80px; +} +</style> diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue index 19763245..5d28c201 100644 --- a/src/views/mp/components/wx-msg/main.vue +++ b/src/views/mp/components/wx-msg/main.vue @@ -7,123 +7,22 @@ --> <template> <ContentWrap> - <div class="msg-div" :id="'msg-div' + nowStr"> + <div class="msg-div" :id="msgDivId"> <!-- 加载更多 --> <div v-loading="loading"></div> <div v-if="!loading"> - <div class="el-table__empty-block" v-if="loadMore" @click="loadingMore" + <div class="el-table__empty-block" v-if="hasMore" @click="loadMore" ><span class="el-table__empty-text">点击加载更多</span></div > - <div class="el-table__empty-block" v-if="!loadMore" + <div class="el-table__empty-block" v-if="!hasMore" ><span class="el-table__empty-text">没有更多了</span></div > </div> + <!-- 消息列表 --> - <div class="execution" v-for="item in list" :key="item.id"> - <div class="avue-comment" :class="item.sendFrom === 2 ? 'avue-comment--reverse' : ''"> - <div class="avatar-div"> - <img - :src="item.sendFrom === 1 ? user.avatar : mp.avatar" - class="avue-comment__avatar" - /> - <div class="avue-comment__author" - >{{ item.sendFrom === 1 ? user.nickname : mp.nickname }} - </div> - </div> - <div class="avue-comment__main"> - <div class="avue-comment__header"> - <div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div> - </div> - <div - class="avue-comment__body" - :style="item.sendFrom === 2 ? 'background: #6BED72;' : ''" - > - <!-- 【事件】区域 --> - <div v-if="item.type === MsgType.Event && item.event === 'subscribe'"> - <el-tag type="success">关注</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'unsubscribe'"> - <el-tag type="danger">取消关注</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'CLICK'"> - <el-tag>点击菜单</el-tag> - 【{{ item.eventKey }}】 - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'VIEW'"> - <el-tag>点击菜单链接</el-tag> - 【{{ item.eventKey }}】 - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_waitmsg'"> - <el-tag>扫码结果</el-tag> - 【{{ item.eventKey }}】 - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'scancode_push'"> - <el-tag>扫码结果</el-tag> - 【{{ item.eventKey }}】 - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'pic_sysphoto'"> - <el-tag>系统拍照发图</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'pic_photo_or_album'"> - <el-tag>拍照或者相册</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'pic_weixin'"> - <el-tag>微信相册</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event && item.event === 'location_select'"> - <el-tag>选择地理位置</el-tag> - </div> - <div v-else-if="item.type === MsgType.Event"> - <el-tag type="danger">未知事件类型</el-tag> - </div> - <!-- 【消息】区域 --> - <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> - <div v-else-if="item.type === MsgType.Voice"> - <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> - </div> - <div v-else-if="item.type === MsgType.Image"> - <a target="_blank" :href="item.mediaUrl"> - <img :src="item.mediaUrl" style="width: 100px" /> - </a> - </div> - <div - v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" - style="text-align: center" - > - <WxVideoPlayer :url="item.mediaUrl" /> - </div> - <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> - <el-link type="success" :underline="false" target="_blank" :href="item.url"> - <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> - </el-link> - <div class="avue-card__info" style="height: unset">{{ item.description }}</div> - </div> - <!-- TODO 芋艿:待完善 --> - <div v-else-if="item.type === MsgType.Location"> - <WxLocation - :label="item.label" - :location-y="item.locationY" - :location-x="item.locationX" - /> - </div> - <div v-else-if="item.type === MsgType.News" style="width: 300px"> - <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> - <WxNews :articles="item.articles" /> - </div> - <div v-else-if="item.type === MsgType.Music"> - <WxMusic - :title="item.title" - :description="item.description" - :thumb-media-url="item.thumbMediaUrl" - :music-url="item.musicUrl" - :hq-music-url="item.hqMusicUrl" - /> - </div> - </div> - </div> - </div> - </div> + <MsgList :list="list" :account-id="accountId" :user="user" /> </div> + <div class="msg-send" v-loading="sendLoading"> <WxReplySelect ref="replySelectRef" v-model="reply" /> <el-button type="success" class="send-but" @click="sendMsg">发送(S)</el-button> @@ -132,18 +31,12 @@ </template> <script setup lang="ts" name="WxMsg"> -import WxReplySelect from '@/views/mp/components/wx-reply' -import WxVideoPlayer from '@/views/mp/components/wx-video-play' -import WxVoicePlayer from '@/views/mp/components/wx-voice-play' -import WxNews from '@/views/mp/components/wx-news' -import WxLocation from '@/views/mp/components/wx-location' -import WxMusic from '@/views/mp/components/wx-music' +import WxReplySelect, { Reply, ReplyType } from '@/views/mp/components/wx-reply' +import MsgList from './components/MsgList.vue' import { getMessagePage, sendMessage } from '@/api/mp/message' import { getUser } from '@/api/mp/user' -import { formatDate } from '@/utils/formatTime' import profile from '@/assets/imgs/profile.jpg' -import wechat from '@/assets/imgs/wechat.png' -import { MsgType } from './types' +import { User } from './types' const message = useMessage() // 消息弹窗 @@ -154,49 +47,30 @@ const props = defineProps({ } }) -const nowStr = ref(new Date().getTime()) // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 +const accountId = ref<number>(-1) // 公众号ID,需要通过userId初始化 +const msgDivId = `msg-div-{new Date().getTime()}` // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 const loading = ref(false) // 消息列表是否正在加载中 -const loadMore = ref(true) // 是否可以加载更多 +const hasMore = ref(true) // 是否可以加载更多 const list = ref<any[]>([]) // 消息列表 const queryParams = reactive({ pageNo: 1, // 当前页数 pageSize: 14, // 每页显示多少条 - accountId: undefined + accountId: accountId }) -interface User { - nickname: string - avatar: string - accountId: number -} // 由于微信不再提供昵称,直接使用“用户”展示 const user: User = reactive({ nickname: '用户', avatar: profile, - accountId: 0 // 公众号账号编号 -}) - -interface Mp { - nickname: string - avatar: string -} -const mp: Mp = reactive({ - nickname: '公众号', - avatar: wechat + accountId: accountId // 公众号账号编号 }) // ========= 消息发送 ========= const sendLoading = ref(false) // 发送消息是否加载中 -interface Reply { - type: MsgType - accountId: number | null - articles: any[] -} - // 微信发送消息 const reply = ref<Reply>({ - type: MsgType.Text, - accountId: null, + type: ReplyType.Text, + accountId: -1, articles: [] }) @@ -207,8 +81,7 @@ onMounted(async () => { const data = await getUser(props.userId) user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname user.avatar = user.avatar?.length > 0 ? data.avatar : user.avatar - user.accountId = data.accountId - queryParams.accountId = data.accountId + accountId.value = data.accountId reply.value.accountId = data.accountId refreshChange() @@ -220,7 +93,11 @@ const sendMsg = async () => { return } // 公众号限制:客服消息,公众号只允许发送一条 - if (reply.value.type === MsgType.News && reply.value.articles.length > 1) { + if ( + reply.value.type === ReplyType.News && + reply.value.articles && + reply.value.articles.length > 1 + ) { reply.value.articles = [reply.value.articles[0]] message.success('图文消息条数限制在 1 条以内,已默认发送第一条') } @@ -229,18 +106,18 @@ const sendMsg = async () => { sendLoading.value = false list.value = [...list.value, ...[data]] - scrollToBottom() + await scrollToBottom() // 发送后清空数据 replySelectRef.value?.clear() } -const loadingMore = () => { +const loadMore = () => { queryParams.pageNo++ getPage(queryParams, null) } -const getPage = async (page, params) => { +const getPage = async (page: any, params: any) => { loading.value = true let dataTemp = await getMessagePage( Object.assign( @@ -254,7 +131,7 @@ const getPage = async (page, params) => { ) ) - const msgDiv = document.getElementById('msg-div' + nowStr.value) + const msgDiv = document.getElementById(msgDivId) let scrollHeight = 0 if (msgDiv) { scrollHeight = msgDiv.scrollHeight @@ -264,24 +141,23 @@ const getPage = async (page, params) => { list.value = [...data, ...list.value] loading.value = false if (data.length < queryParams.pageSize || data.length === 0) { - loadMore.value = false + hasMore.value = false } queryParams.pageNo = page.pageNo queryParams.pageSize = page.pageSize // 滚动到原来的位置 if (queryParams.pageNo === 1) { // 定位到消息底部 - scrollToBottom() + await scrollToBottom() } else if (data.length !== 0) { // 定位滚动条 - await nextTick(() => { - if (scrollHeight !== 0) { - let div = document.getElementById('msg-div' + nowStr.value) - if (div && msgDiv) { - msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100 - } + await nextTick() + if (scrollHeight !== 0) { + let div = document.getElementById(msgDivId) + if (div && msgDiv) { + msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100 } - }) + } } } @@ -290,26 +166,16 @@ const refreshChange = () => { } /** 定位到消息底部 */ -const scrollToBottom = () => { - nextTick(() => { - let div = document.getElementById('msg-div' + nowStr.value) - if (div) { - div.scrollTop = div.scrollHeight - } - }) +const scrollToBottom = async () => { + await nextTick() + let div = document.getElementById(msgDivId) + if (div) { + div.scrollTop = div.scrollHeight + } } </script> <style lang="scss" scoped> -/* 因为 joolun 实现依赖 avue 组件,该页面使用了 comment.scss、card.scc */ -@import './comment.scss'; -@import './card.scss'; - -.msg-main { - margin-top: -30px; - padding: 10px; -} - .msg-div { height: 50vh; overflow: auto; @@ -322,11 +188,6 @@ const scrollToBottom = () => { padding: 10px; } -.avatar-div { - text-align: center; - width: 80px; -} - .send-but { float: right; margin-top: 8px; diff --git a/src/views/mp/components/wx-msg/types.ts b/src/views/mp/components/wx-msg/types.ts index b1989ea7..38a0ff86 100644 --- a/src/views/mp/components/wx-msg/types.ts +++ b/src/views/mp/components/wx-msg/types.ts @@ -9,3 +9,9 @@ export enum MsgType { Music = 'music', News = 'news' } + +export interface User { + nickname: string + avatar: string + accountId: number +} From 036c9b33663b8aa82b0bd0a25b714348a23e32db Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Sat, 22 Apr 2023 20:52:11 +0800 Subject: [PATCH 03/28] =?UTF-8?q?fix:=20mp=E6=A8=A1=E5=9D=97=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=B0=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/components/wx-music/main.vue | 4 ++-- src/views/mp/draft/components/CoverSelect.vue | 6 +++--- src/views/mp/draft/components/NewsForm.vue | 2 +- src/views/mp/draft/index.vue | 17 ++++------------- src/views/mp/menu/index.vue | 2 +- 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue index 70f4c58b..f528359d 100644 --- a/src/views/mp/components/wx-music/main.vue +++ b/src/views/mp/components/wx-music/main.vue @@ -55,6 +55,6 @@ defineExpose({ </script> <style lang="scss" scoped> -/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scc */ -@import url('../wx-msg/card.scss'); +/* 因为 joolun 实现依赖 avue 组件,该页面使用了 card.scss */ +@import '../wx-msg/card.scss'; </style> diff --git a/src/views/mp/draft/components/CoverSelect.vue b/src/views/mp/draft/components/CoverSelect.vue index 944b7d96..bbb2b44c 100644 --- a/src/views/mp/draft/components/CoverSelect.vue +++ b/src/views/mp/draft/components/CoverSelect.vue @@ -51,7 +51,7 @@ > <WxMaterialSelect type="image" - :account-id="accountId" + :account-id="accountId!" @select-material="onMaterialSelected" /> </el-dialog> @@ -93,11 +93,11 @@ const showImageDialog = ref(false) const fileList = ref<UploadFiles>([]) interface UploadData { type: UploadType - accountId: number | undefined + accountId: number } const uploadData: UploadData = reactive({ type: UploadType.Image, - accountId: accountId + accountId: accountId! }) /** 素材选择完成事件*/ diff --git a/src/views/mp/draft/components/NewsForm.vue b/src/views/mp/draft/components/NewsForm.vue index a2b88a5b..97166a4b 100644 --- a/src/views/mp/draft/components/NewsForm.vue +++ b/src/views/mp/draft/components/NewsForm.vue @@ -125,7 +125,7 @@ </el-container> </template> -<script setup lang="ts"> +<script setup lang="ts" name="NewsForm"> import { Editor } from '@/components/Editor' import { createEditorConfig } from '../editor-config' import CoverSelect from './CoverSelect.vue' diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue index d8e771a0..6b40bc35 100644 --- a/src/views/mp/draft/index.vue +++ b/src/views/mp/draft/index.vue @@ -76,7 +76,7 @@ import { const message = useMessage() // 消息 -const accountId = ref<number>(0) +const accountId = ref<number>(-1) provide('accountId', accountId) const loading = ref(true) // 列表的加载中 @@ -90,16 +90,7 @@ interface QueryParams { const queryParams: QueryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0 -}) - -interface UploadData { - type: 'image' | 'video' | 'audio' - accountId: number -} -const uploadData: UploadData = reactive({ - type: 'image', - accountId: 0 + accountId: accountId }) // ========== 草稿新建 or 修改 ========== @@ -126,8 +117,8 @@ const onBeforeDialogClose = async (onDone: () => {}) => { // ======================== 列表查询 ======================== /** 设置账号编号 */ const setAccountId = (id: number) => { - queryParams.accountId = id - uploadData.accountId = id + accountId.value = id + // queryParams.accountId = id } /** 查询列表 */ diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue index 442e33b5..cbec87dc 100644 --- a/src/views/mp/menu/index.vue +++ b/src/views/mp/menu/index.vue @@ -339,7 +339,7 @@ div { .left { position: relative; - display: inline-block; + display: block; float: left; width: 350px; height: 715px; From 198752868ca8c3f84f250783b95d60fa3de9919c Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Sat, 22 Apr 2023 23:43:35 +0800 Subject: [PATCH 04/28] =?UTF-8?q?refactor:=20mp=E6=A8=A1=E5=9D=97=E7=BB=9F?= =?UTF-8?q?=E4=B8=80accountId=E6=9C=AA=E5=88=9D=E5=A7=8B=E5=8C=96=E5=80=BC?= =?UTF-8?q?=E4=B8=BA-1=EF=BC=8C=E5=88=A0=E9=99=A4QueryParams=E5=AE=9A?= =?UTF-8?q?=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mp/account/index.ts | 2 +- src/views/mp/autoReply/index.vue | 15 ++++------ .../mp/components/wx-account-select/main.vue | 7 +++-- src/views/mp/draft/index.vue | 29 ++++++------------- src/views/mp/freePublish/index.vue | 10 ++----- src/views/mp/material/index.vue | 11 ++----- src/views/mp/menu/index.vue | 2 +- src/views/mp/message/index.vue | 17 ++++------- src/views/mp/statistics/index.vue | 4 +-- src/views/mp/tag/index.vue | 11 ++----- src/views/mp/user/index.vue | 17 ++++------- 11 files changed, 42 insertions(+), 83 deletions(-) diff --git a/src/api/mp/account/index.ts b/src/api/mp/account/index.ts index d641ef3c..e973cda3 100644 --- a/src/api/mp/account/index.ts +++ b/src/api/mp/account/index.ts @@ -1,7 +1,7 @@ import request from '@/config/axios' export interface AccountVO { - id?: number + id: number name: string } diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue index 20a1e683..e2fcd7a0 100644 --- a/src/views/mp/autoReply/index.vue +++ b/src/views/mp/autoReply/index.vue @@ -103,6 +103,7 @@ import ReplyTable from './components/ReplyTable.vue' import { MsgType } from './components/types' const message = useMessage() // 消息 +const accountId = ref(-1) // 公众号ID const msgType = ref<MsgType>(MsgType.Keyword) // 消息类型 const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型 const loading = ref(true) // 遮罩层 @@ -110,15 +111,10 @@ const total = ref(0) // 总条数 const list = ref<any[]>([]) // 自动回复列表 const formRef = ref<FormInstance | null>(null) // 表单 ref // 查询参数 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0 + accountId: accountId }) const dialogTitle = ref('') // 弹出层标题 @@ -127,7 +123,7 @@ const replyForm = ref<any>({}) // 表单参数 // 回复消息 const reply = ref<Reply>({ type: ReplyType.Text, - accountId: 0 + accountId: -1 }) // 表单校验 const rules = { @@ -137,8 +133,9 @@ const rules = { /** 侦听账号变化 */ const onAccountChanged = (id: number) => { - queryParams.accountId = id + accountId.value = id reply.value.accountId = id + queryParams.pageNo = 1 getList() } diff --git a/src/views/mp/components/wx-account-select/main.vue b/src/views/mp/components/wx-account-select/main.vue index 8dbad499..e2501657 100644 --- a/src/views/mp/components/wx-account-select/main.vue +++ b/src/views/mp/components/wx-account-select/main.vue @@ -8,13 +8,14 @@ import * as MpAccountApi from '@/api/mp/account' const account: MpAccountApi.AccountVO = reactive({ - id: undefined, + id: -1, name: '' }) -const accountList: Ref<MpAccountApi.AccountVO[]> = ref([]) + +const accountList = ref<MpAccountApi.AccountVO[]>([]) const emit = defineEmits<{ - (e: 'change', id: number, name: string): void + (e: 'change', id: number, name: string) }>() const handleQuery = async () => { diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue index 6b40bc35..a916ed39 100644 --- a/src/views/mp/draft/index.vue +++ b/src/views/mp/draft/index.vue @@ -76,18 +76,14 @@ import { const message = useMessage() // 消息 -const accountId = ref<number>(-1) +const accountId = ref(-1) provide('accountId', accountId) const loading = ref(true) // 列表的加载中 const list = ref<any[]>([]) // 列表的数据 const total = ref(0) // 列表的总页数 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number -} -const queryParams: QueryParams = reactive({ + +const queryParams = reactive({ pageNo: 1, pageSize: 10, accountId: accountId @@ -102,7 +98,8 @@ const isSubmitting = ref(false) /** 侦听公众号变化 **/ const onAccountChanged = (id: number) => { - setAccountId(id) + accountId.value = id + queryParams.pageNo = 1 getList() } @@ -115,12 +112,6 @@ const onBeforeDialogClose = async (onDone: () => {}) => { } // ======================== 列表查询 ======================== -/** 设置账号编号 */ -const setAccountId = (id: number) => { - accountId.value = id - // queryParams.accountId = id -} - /** 查询列表 */ const getList = async () => { loading.value = true @@ -161,10 +152,10 @@ const onSubmitNewsItem = async () => { isSubmitting.value = true try { if (isCreating.value) { - await MpDraftApi.createDraft(queryParams.accountId, newsList.value) + await MpDraftApi.createDraft(accountId.value, newsList.value) message.notifySuccess('新增成功') } else { - await MpDraftApi.updateDraft(queryParams.accountId, mediaId.value, newsList.value) + await MpDraftApi.updateDraft(accountId.value, mediaId.value, newsList.value) message.notifySuccess('更新成功') } } finally { @@ -176,7 +167,6 @@ const onSubmitNewsItem = async () => { // ======================== 草稿箱发布 ======================== const onPublish = async (item: Article) => { - const accountId = queryParams.accountId const mediaId = item.mediaId const content = '你正在通过发布的方式发表内容。 发布不占用群发次数,一天可多次发布。' + @@ -184,7 +174,7 @@ const onPublish = async (item: Article) => { '发布后,你可以前往发表记录获取链接,也可以将发布内容添加到自定义菜单、自动回复、话题和页面模板中。' try { await message.confirm(content) - await MpFreePublishApi.submitFreePublish(accountId, mediaId) + await MpFreePublishApi.submitFreePublish(accountId.value, mediaId) message.notifySuccess('发布成功') await getList() } catch {} @@ -192,11 +182,10 @@ const onPublish = async (item: Article) => { /** 删除按钮操作 */ const onDelete = async (item: Article) => { - const accountId = queryParams.accountId const mediaId = item.mediaId try { await message.confirm('此操作将永久删除该草稿, 是否继续?') - await MpDraftApi.deleteDraft(accountId, mediaId) + await MpDraftApi.deleteDraft(accountId.value, mediaId) message.notifySuccess('删除成功') await getList() } catch {} diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue index 08a202c2..62ca1999 100644 --- a/src/views/mp/freePublish/index.vue +++ b/src/views/mp/freePublish/index.vue @@ -59,20 +59,16 @@ const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref<any[]>([]) // 列表的数据 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0 + accountId: -1 }) /** 侦听公众号变化 **/ const onAccountChanged = (id: number) => { queryParams.accountId = id + queryParams.pageNo = 1 getList() } diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue index 0e2a87d6..b72c9ad6 100644 --- a/src/views/mp/material/index.vue +++ b/src/views/mp/material/index.vue @@ -100,16 +100,10 @@ const loading = ref(false) // 遮罩层 const list = ref<any[]>([]) // 总条数 const total = ref(0) // 数据列表 // 查询参数 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number - permanent: boolean -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0, + accountId: -1, permanent: true }) const showCreateVideo = ref(false) // 是否新建视频的弹窗 @@ -117,6 +111,7 @@ const showCreateVideo = ref(false) // 是否新建视频的弹窗 /** 侦听公众号变化 **/ const onAccountChanged = (id: number) => { queryParams.accountId = id + queryParams.pageNo = 1 getList() } diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue index cbec87dc..0b02cc16 100644 --- a/src/views/mp/menu/index.vue +++ b/src/views/mp/menu/index.vue @@ -65,7 +65,7 @@ const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__' // ======================== 列表查询 ======================== const loading = ref(false) // 遮罩层 -const accountId = ref<number>(0) +const accountId = ref(-1) const accountName = ref<string>('') const menuList = ref<Menu[]>([]) diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue index 85048f38..db92cc0f 100644 --- a/src/views/mp/message/index.vue +++ b/src/views/mp/message/index.vue @@ -93,20 +93,12 @@ const total = ref(0) // 数据的总页数 const list = ref<any[]>([]) // 当前页的列表数据 // 搜索参数 -interface QueryParams { - pageNo: number - pageSize: number - openid: string | undefined - accountId: number - type: MsgType | undefined - createTime: string[] | [] -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - openid: undefined, - accountId: 0, - type: undefined, + openid: '', + accountId: -1, + type: MsgType.Text, createTime: [] }) const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 @@ -120,6 +112,7 @@ const messageBox = reactive({ /** 侦听accountId */ const onAccountChanged = (id: number) => { queryParams.accountId = id + queryParams.pageNo = 1 handleQuery() } diff --git a/src/views/mp/statistics/index.vue b/src/views/mp/statistics/index.vue index cef8e079..4e2dbfcc 100644 --- a/src/views/mp/statistics/index.vue +++ b/src/views/mp/statistics/index.vue @@ -84,7 +84,7 @@ const dateRange = ref([ beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)), endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)) ]) -const accountId = ref() // 选中的公众号编号 +const accountId = ref(-1) // 选中的公众号编号 const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表 const xAxisDate = ref([] as any[]) // X 轴的日期范围 @@ -232,7 +232,7 @@ const getAccountList = async () => { accountList.value = await MpAccountApi.getSimpleAccountList() // 默认选中第一个 if (accountList.value.length > 0) { - accountId.value = accountList.value[0].id + accountId.value = accountList.value[0].id! } } diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue index a92d9127..8d452a5d 100644 --- a/src/views/mp/tag/index.vue +++ b/src/views/mp/tag/index.vue @@ -95,23 +95,18 @@ const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref<any[]>([]) // 列表的数据 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0 + accountId: -1 }) const formRef = ref<InstanceType<typeof TagForm> | null>(null) /** 侦听公众号变化 **/ const onAccountChanged = (id: number) => { - queryParams.pageNo = 1 queryParams.accountId = id + queryParams.pageNo = 1 getList() } diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue index 03e58a7f..422e219b 100644 --- a/src/views/mp/user/index.vue +++ b/src/views/mp/user/index.vue @@ -113,27 +113,20 @@ const loading = ref(true) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref<any[]>([]) // 列表的数据 -interface QueryParams { - pageNo: number - pageSize: number - accountId: number - openid: string | null - nickname: string | null -} -const queryParams: QueryParams = reactive({ +const queryParams = reactive({ pageNo: 1, pageSize: 10, - accountId: 0, - openid: null, - nickname: null + accountId: -1, + openid: '', + nickname: '' }) const queryFormRef = ref<FormInstance | null>(null) // 搜索的表单 const tagList = ref<any[]>([]) // 公众号标签列表 /** 侦听公众号变化 **/ const onAccountChanged = (id: number) => { - queryParams.pageNo = 1 queryParams.accountId = id + queryParams.pageNo = 1 getList() } From cb4527748e70a74b616a095614503937887504a0 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Sat, 22 Apr 2023 23:45:14 +0800 Subject: [PATCH 05/28] =?UTF-8?q?refactor:=20mp/wx-msg=20=E9=87=87?= =?UTF-8?q?=E7=94=A8ref=E6=9D=A5=E5=AE=9E=E7=8E=B0=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/components/wx-msg/main.vue | 30 ++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue index 5d28c201..079e9740 100644 --- a/src/views/mp/components/wx-msg/main.vue +++ b/src/views/mp/components/wx-msg/main.vue @@ -7,7 +7,7 @@ --> <template> <ContentWrap> - <div class="msg-div" :id="msgDivId"> + <div class="msg-div" ref="msgDivRef"> <!-- 加载更多 --> <div v-loading="loading"></div> <div v-if="!loading"> @@ -47,8 +47,7 @@ const props = defineProps({ } }) -const accountId = ref<number>(-1) // 公众号ID,需要通过userId初始化 -const msgDivId = `msg-div-{new Date().getTime()}` // 当前的时间戳,用于每次消息加载后,回到原位置;具体见 :id="'msg-div' + nowStr" 处 +const accountId = ref(-1) // 公众号ID,需要通过userId初始化 const loading = ref(false) // 消息列表是否正在加载中 const hasMore = ref(true) // 是否可以加载更多 const list = ref<any[]>([]) // 消息列表 @@ -74,7 +73,8 @@ const reply = ref<Reply>({ articles: [] }) -const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) +const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelect组件ref,用于消息发送成功后清除内容 +const msgDivRef = ref() // 消息显示窗口ref,用于滚动到底部 /** 完成加载 */ onMounted(async () => { @@ -89,7 +89,7 @@ onMounted(async () => { // 执行发送 const sendMsg = async () => { - if (!reply) { + if (!unref(reply)) { return } // 公众号限制:客服消息,公众号只允许发送一条 @@ -117,7 +117,7 @@ const loadMore = () => { getPage(queryParams, null) } -const getPage = async (page: any, params: any) => { +const getPage = async (page: any, params: any = null) => { loading.value = true let dataTemp = await getMessagePage( Object.assign( @@ -131,11 +131,7 @@ const getPage = async (page: any, params: any) => { ) ) - const msgDiv = document.getElementById(msgDivId) - let scrollHeight = 0 - if (msgDiv) { - scrollHeight = msgDiv.scrollHeight - } + const scrollHeight = msgDivRef.value?.scrollHeight ?? 0 // 处理数据 const data = dataTemp.list.reverse() list.value = [...data, ...list.value] @@ -153,24 +149,22 @@ const getPage = async (page: any, params: any) => { // 定位滚动条 await nextTick() if (scrollHeight !== 0) { - let div = document.getElementById(msgDivId) - if (div && msgDiv) { - msgDiv.scrollTop = div.scrollHeight - scrollHeight - 100 + if (msgDivRef.value) { + msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight - scrollHeight - 100 } } } } const refreshChange = () => { - getPage(queryParams, null) + getPage(queryParams) } /** 定位到消息底部 */ const scrollToBottom = async () => { await nextTick() - let div = document.getElementById(msgDivId) - if (div) { - div.scrollTop = div.scrollHeight + if (msgDivRef.value) { + msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight } } </script> From 9dc361e708b2251487d41ac4abd5b6630caa405b Mon Sep 17 00:00:00 2001 From: AhJindeg <AhJindeg@163.com> Date: Sun, 23 Apr 2023 11:44:03 +0800 Subject: [PATCH 06/28] =?UTF-8?q?=F0=9F=8C=88=20style(Form/src):=20Modifyi?= =?UTF-8?q?ng=20word=20spelling=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改ts类型 PlaceholderModel单词拼写 - 修改initModel方法 FormModel注释拼写 --- src/components/Form/src/helper.ts | 8 ++++---- src/components/Form/src/types.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts index 9cab8ff1..cdfc8caa 100644 --- a/src/components/Form/src/helper.ts +++ b/src/components/Form/src/helper.ts @@ -1,6 +1,6 @@ import type { Slots } from 'vue' import { getSlot } from '@/utils/tsxHelper' -import { PlaceholderMoel } from './types' +import { PlaceholderModel } from './types' import { FormSchema } from '@/types/form' import { ColProps } from '@/types/components' @@ -10,7 +10,7 @@ import { ColProps } from '@/types/components' * @returns 返回提示信息对象 * @description 用于自动设置placeholder */ -export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => { +export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { const { t } = useI18n() const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword'] const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect'] @@ -108,8 +108,8 @@ export const setItemComponentSlots = ( /** * * @param schema Form表单结构化数组 - * @param formModel FormMoel - * @returns FormMoel + * @param formModel FormModel + * @returns FormModel * @description 生成对应的formModel */ export const initModel = (schema: FormSchema[], formModel: Recordable) => { diff --git a/src/components/Form/src/types.ts b/src/components/Form/src/types.ts index 92a49d85..dcd01e78 100644 --- a/src/components/Form/src/types.ts +++ b/src/components/Form/src/types.ts @@ -1,6 +1,6 @@ import { FormSchema } from '@/types/form' -export interface PlaceholderMoel { +export interface PlaceholderModel { placeholder?: string startPlaceholder?: string endPlaceholder?: string From a0014bed6529b15e07cf96413d4702c9904682a8 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Mon, 24 Apr 2023 11:42:44 +0800 Subject: [PATCH 07/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E7=95=8C=E9=9D=A2=E7=BB=93=E6=9E=84=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/modules/remaining.ts | 31 ++- src/views/mall/product/management/addForm.vue | 53 +++++ .../management/components/BasicInfoForm.vue | 191 +++++++++++++++ .../management/components/DescriptionForm.vue | 13 + .../components/OtherSettingsForm.vue | 94 ++++++++ .../product/management/components/index.ts | 5 + src/views/mall/product/management/index.vue | 225 ++++++++++++++++++ 7 files changed, 608 insertions(+), 4 deletions(-) create mode 100644 src/views/mall/product/management/addForm.vue create mode 100644 src/views/mall/product/management/components/BasicInfoForm.vue create mode 100644 src/views/mall/product/management/components/DescriptionForm.vue create mode 100644 src/views/mall/product/management/components/OtherSettingsForm.vue create mode 100644 src/views/mall/product/management/components/index.ts create mode 100644 src/views/mall/product/management/index.vue diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 55e933ed..32848b9a 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -2,9 +2,9 @@ import { Layout } from '@/utils/routerHelper' const { t } = useI18n() /** -* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击 -* name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 -* meta : { + * redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击 + * name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 + * meta : { hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false) alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式, @@ -31,7 +31,7 @@ const { t } = useI18n() canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false) } -**/ + **/ const remainingRouter: AppRouteRecordRaw[] = [ { path: '/redirect', @@ -345,6 +345,29 @@ const remainingRouter: AppRouteRecordRaw[] = [ meta: { title: '商品属性值', icon: '', activeMenu: '/product/property' } } ] + }, + { + path: '/product', + component: Layout, + name: 'ProductManagementEdit', + meta: { + hidden: true + }, + children: [ + { + path: 'productManagementAdd', + component: () => import('@/views/mall/product/management/addForm.vue'), + name: 'ProductManagementAdd', + meta: { + noCache: true, + hidden: true, + canTo: true, + icon: 'ep:edit', + title: '添加商品', + activeMenu: '/product/product-management' + } + } + ] } ] diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue new file mode 100644 index 00000000..d077df5b --- /dev/null +++ b/src/views/mall/product/management/addForm.vue @@ -0,0 +1,53 @@ +<template> + <ContentWrap v-loading="formLoading"> + <el-tabs v-model="activeName"> + <el-tab-pane label="商品信息" name="basicInfo"> + <BasicInfoForm ref="basicInfoRef" /> + </el-tab-pane> + <el-tab-pane label="商品详情" name="description"> + <DescriptionForm ref="DescriptionRef" /> + </el-tab-pane> + <el-tab-pane label="其他设置" name="otherSettings"> + <OtherSettingsForm ref="otherSettingsRef" /> + </el-tab-pane> + </el-tabs> + <el-form> + <el-form-item style="float: right"> + <el-button :loading="formLoading" type="primary" @click="submitForm">保存</el-button> + <el-button @click="close">返回</el-button> + </el-form-item> + </el-form> + </ContentWrap> +</template> +<script lang="ts" name="ProductManagementForm" setup> +import { useTagsViewStore } from '@/store/modules/tagsView' +import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' + +// const { t } = useI18n() // 国际化 +// const message = useMessage() // 消息弹窗 +const { push, currentRoute } = useRouter() // 路由 +// const { query } = useRoute() // 查询参数 +const { delView } = useTagsViewStore() // 视图操作 + +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const activeName = ref('otherSettings') // Tag 激活的窗口 +const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() +const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() + +/** 获得详情 */ +const getDetail = async () => {} + +/** 提交按钮 */ +const submitForm = async () => {} + +/** 关闭按钮 */ +const close = () => { + delView(unref(currentRoute)) + push('/product/product-management') +} + +/** 初始化 */ +onMounted(() => { + getDetail() +}) +</script> diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue new file mode 100644 index 00000000..1b33e9eb --- /dev/null +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -0,0 +1,191 @@ +<template> + <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px"> + <el-row> + <el-col :span="12"> + <el-form-item label="商品名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入商品名称" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商品分类" prop="categoryId"> + <el-tree-select + v-model="formData.categoryId" + :data="[]" + :props="defaultProps" + check-strictly + node-key="id" + placeholder="请选择商品分类" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商品关键字" prop="keyword"> + <el-input v-model="formData.keyword" placeholder="请输入商品关键字" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="单位" prop="unit"> + <el-input v-model="formData.unit" placeholder="请输入单位" /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商品简介" prop="introduction"> + <el-input + v-model="formData.introduction" + :rows="3" + placeholder="请输入商品简介" + type="textarea" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="商品封面图" prop="picUrl"> + <div class="demo-image__preview pt-5px pb-5px pl-11x pr-11px"> + <el-image + :initial-index="0" + :preview-src-list="srcList" + :src="url" + :zoom-rate="1.2" + fit="cover" + style="width: 100%; height: 90px" + /> + </div> + </el-form-item> + </el-col> + <el-col :span="24"> + <el-form-item label="商品轮播图" prop="sliderPicUrls"> + <el-button>添加轮播图</el-button> + <el-carousel :interval="3000" height="200px" style="width: 100%" type="card"> + <el-carousel-item v-for="item in 6" :key="item"> + <h3 justify="center" text="2xl">{{ item }}</h3> + </el-carousel-item> + </el-carousel> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="运费模板" prop="deliveryTemplateId"> + <el-select v-model="formData.deliveryTemplateId" placeholder="请选择" style="width: 100%"> + <el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" /> + </el-select> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-button class="ml-20px">运费模板</el-button> + </el-col> + <el-col :span="12"> + <el-form-item label="商品规格" props="specType"> + <el-radio-group v-model="formData.specType" @change="changeSpecType(formData.specType)"> + <el-radio :label="false" class="radio">单规格</el-radio> + <el-radio :label="true">多规格</el-radio> + </el-radio-group> + </el-form-item> + </el-col> + <!-- TODO 商品规格和分销类型切换待定 --> + <el-col :span="12"> + <el-form-item label="分销类型" props="subCommissionType"> + <el-radio-group + v-model="formData.subCommissionType" + @change="changeSubCommissionType(formData.subCommissionType)" + > + <el-radio :label="false">默认设置</el-radio> + <el-radio :label="true" class="radio">自行设置</el-radio> + </el-radio-group> + </el-form-item> + </el-col> + <!-- 多规格添加--> + <el-col v-if="formData.specType" :span="24"> + <el-form-item label="选择规格" prop=""> + <div class="acea-row"> + <el-select v-model="formData.selectRule"> + <el-option + v-for="item in []" + :key="item.id" + :label="item.ruleName" + :value="item.id" + /> + </el-select> + <el-button class="mr-20px" type="primary" @click="confirm">确认</el-button> + <el-button class="mr-15px" @click="addRule">添加规格</el-button> + </div> + </el-form-item> + </el-col> + </el-row> + </el-form> +</template> +<script lang="ts" name="ProductManagementBasicInfoForm" setup> +// TODO 商品封面测试数据 +import { defaultProps } from '@/utils/tree' + +const url = 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg' +const srcList = ['https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'] + +const formRef = ref() +const formData = ref({ + name: '', // 商品名称 + categoryId: '', // 商品分类 + keyword: '', // 关键字 + unit: '', // 单位 + picUrl: '', // 商品封面图 + sliderPicUrls: [], // 商品轮播图 + introduction: '', // 商品简介 + deliveryTemplateId: '', // 运费模版 + selectRule: '', + specType: false, // 商品规格 + subCommissionType: false // 分销类型 +}) +const rules = reactive({ + name: [required], + categoryId: [required], + keyword: [required], + unit: [required], + picUrl: [required], + sliderPicUrls: [required], + deliveryTemplateId: [required], + specType: [required], + subCommissionType: [required] +}) +// 选择规格 +const changeSpecType = (specType) => { + console.log(specType) +} +// 分销类型 +const changeSubCommissionType = (subCommissionType) => { + console.log(subCommissionType) +} +// 选择属性确认 +const confirm = () => {} +// 添加规格 +const addRule = () => {} +</script> +<style scoped> +/*TODO 商品轮播图测试样式*/ +.el-carousel__item h3 { + color: #475669; + opacity: 0.75; + line-height: 200px; + margin: 0; + text-align: center; +} + +.el-carousel__item:nth-child(2n) { + background-color: #99a9bf; +} + +.el-carousel__item:nth-child(2n + 1) { + background-color: #d3dce6; +} + +/*TODO 商品封面测试样式*/ +.demo-image__error .image-slot { + font-size: 30px; +} + +.demo-image__error .image-slot .el-icon { + font-size: 30px; +} + +.demo-image__error .el-image { + width: 100%; + height: 200px; +} +</style> diff --git a/src/views/mall/product/management/components/DescriptionForm.vue b/src/views/mall/product/management/components/DescriptionForm.vue new file mode 100644 index 00000000..53609705 --- /dev/null +++ b/src/views/mall/product/management/components/DescriptionForm.vue @@ -0,0 +1,13 @@ +<template> + <!--富文本编辑器组件--> + <el-row> + <Editor v-model="content" :editor-config="editorConfig" /> + </el-row> +</template> +<script lang="ts" name="DescriptionForm" setup> +import { Editor } from '@/components/Editor' +import { createEditorConfig } from '@/views/mp/draft/editor-config' +// TODO 模拟参数 +const content = ref('') +const editorConfig = createEditorConfig('', 1) +</script> diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue new file mode 100644 index 00000000..e8152883 --- /dev/null +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -0,0 +1,94 @@ +<template> + <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px"> + <el-row> + <el-col :span="24"> + <el-col :span="8"> + <el-form-item label="商品排序"> + <el-input-number v-model="formData.sort" :min="0" /> + </el-form-item> + </el-col> + <el-col :span="8"> + <el-form-item label="赠送积分"> + <el-input-number v-model="formData.giveIntegral" :min="0" /> + </el-form-item> + </el-col> + <el-col :span="8"> + <el-form-item label="虚拟销量"> + <el-input-number + v-model="formData.virtualSalesCount" + :min="0" + placeholder="请输入虚拟销量" + /> + </el-form-item> + </el-col> + </el-col> + <el-col :span="24"> + <el-form-item label="商品推荐"> + <el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup"> + <el-checkbox v-for="(item, index) in recommend" :key="index" :label="item.value"> + {{ item.name }} + </el-checkbox> + </el-checkbox-group> + </el-form-item> + </el-col> + <el-col :span="24"> + <!-- TODO tag展示暂时不考虑排序 --> + <el-form-item label="活动优先级"> + <el-tag>默认</el-tag> + <el-tag class="ml-2" type="success">秒杀</el-tag> + <el-tag class="ml-2" type="info">砍价</el-tag> + <el-tag class="ml-2" type="warning">拼团</el-tag> + </el-form-item> + </el-col> + <el-col :span="24"> + <el-form-item label="赠送优惠劵"> + <el-button>选择优惠券</el-button> + </el-form-item> + </el-col> + </el-row> + </el-form> +</template> +<script lang="ts" name="OtherSettingsForm" setup> +// 商品推荐 +const recommend = [ + { name: '是否热卖', value: 'recommendHot' }, + { name: '是否优惠', value: 'recommendBenefit' }, + { name: '是否精品', value: 'recommendBest' }, + { name: '是否新品', value: 'recommendNew' }, + { name: '是否优品', value: 'recommendGood' } +] +const checkboxGroup = ref<string[]>([]) +const onChangeGroup = () => { + checkboxGroup.value.includes('recommendHot') + ? (formData.value.recommendHot = true) + : (formData.value.recommendHot = false) + checkboxGroup.value.includes('recommendBenefit') + ? (formData.value.recommendBenefit = true) + : (formData.value.recommendBenefit = false) + checkboxGroup.value.includes('recommendBest') + ? (formData.value.recommendBest = true) + : (formData.value.recommendBest = false) + checkboxGroup.value.includes('recommendNew') + ? (formData.value.recommendNew = true) + : (formData.value.recommendNew = false) + checkboxGroup.value.includes('recommendGood') + ? (formData.value.recommendGood = true) + : (formData.value.recommendGood = false) +} +const formRef = ref() +const formData = ref({ + sort: '', + giveIntegral: 666, + virtualSalesCount: 565656, + recommendHot: false, + recommendBenefit: false, + recommendBest: false, + recommendNew: false, + recommendGood: false +}) +const rules = reactive({ + sort: [required], + giveIntegral: [required], + virtualSalesCount: [required] +}) +</script> diff --git a/src/views/mall/product/management/components/index.ts b/src/views/mall/product/management/components/index.ts new file mode 100644 index 00000000..04e6f74d --- /dev/null +++ b/src/views/mall/product/management/components/index.ts @@ -0,0 +1,5 @@ +import BasicInfoForm from './BasicInfoForm.vue' +import DescriptionForm from './DescriptionForm.vue' +import OtherSettingsForm from './OtherSettingsForm.vue' + +export { BasicInfoForm, DescriptionForm, OtherSettingsForm } diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/management/index.vue new file mode 100644 index 00000000..4fdfed1b --- /dev/null +++ b/src/views/mall/product/management/index.vue @@ -0,0 +1,225 @@ +<template> + <!-- 搜索工作栏 --> + <ContentWrap> + <el-form + ref="queryFormRef" + :inline="true" + :model="queryParams" + class="-mb-15px" + label-width="68px" + > + <el-form-item label="品牌名称" prop="name"> + <el-input + v-model="queryParams.name" + class="!w-240px" + clearable + placeholder="请输入品牌名称" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="状态" prop="status"> + <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-240px" + end-placeholder="结束日期" + start-placeholder="开始日期" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> + <el-button + v-hasPermi="['product:brand:create']" + plain + type="primary" + @click="openForm('create')" + > + <Icon class="mr-5px" icon="ep:plus" /> + 新增 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap> + <el-tabs v-model="queryParams.type" @tab-click="getList"> + <el-tab-pane + v-for="(item, index) in headerNum" + :key="index" + :label="item.name + '(' + item.count + ')'" + :name="item.type.toString()" + /> + </el-tabs> + <el-table v-loading="loading" :data="list"> + <el-table-column type="expand"> + <template #default="{ row }"> + <el-form class="demo-table-expand" inline label-position="left"> + <el-form-item label="市场价:"> + <span>{{ row.marketPrice }}</span> + </el-form-item> + <el-form-item label="成本价:"> + <span>{{ row.costPrice }}</span> + </el-form-item> + <el-form-item label="虚拟销量:"> + <span>{{ row.virtualSalesCount }}</span> + </el-form-item> + </el-form> + </template> + </el-table-column> + <el-table-column label="商品图" min-width="80"> + <template #default="{ row }"> + <div class="demo-image__preview"> + <el-image + :preview-src-list="[row.image]" + :src="row.image" + style="width: 36px; height: 36px" + /> + </div> + </template> + </el-table-column> + <el-table-column + :show-overflow-tooltip="true" + label="商品名称" + min-width="300" + prop="storeName" + /> + <el-table-column align="center" label="商品售价" min-width="90" prop="price" /> + <el-table-column align="center" label="销量" min-width="90" prop="sales" /> + <el-table-column align="center" label="库存" min-width="90" prop="stock" /> + <el-table-column align="center" label="排序" min-width="70" prop="sort" /> + <el-table-column + :formatter="dateFormatter" + align="center" + label="创建时间" + prop="createTime" + width="180" + /> + <el-table-column fixed="right" label="状态" min-width="80"> + <template #default="{ row }"> + <!--TODO 暂时用COMMON_STATUS占位一下使其不报错 --> + <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" /> + </template> + </el-table-column> + <el-table-column align="center" fixed="right" label="操作" min-width="150" /> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + </ContentWrap> +</template> +<script lang="ts" name="ProductManagement" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import { dateFormatter } from '@/utils/formatTime' + +// const message = useMessage() // 消息弹窗 +// const { t } = useI18n() // 国际化 +const { push } = useRouter() // 路由跳转 +const loading = ref(false) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref<any[]>([]) // 列表的数据 +const headerNum = ref([ + { + count: 8, + name: '出售中商品', + type: 1 + }, + { + count: 0, + name: '仓库中商品', + type: 2 + }, + { + count: 0, + name: '已经售馨商品', + type: 3 + }, + { + count: 0, + name: '警戒库存', + type: 4 + }, + { + count: 0, + name: '商品回收站', + type: 5 + } +]) +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + name: undefined, + status: undefined, + createTime: [], + type: '1' +}) +const queryFormRef = ref() // 搜索的表单 + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + // const data = await ProductBrandApi.getBrandParam(queryParams) + // list.value = data.list + // total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + getList() +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +const openForm = () => { + push('/product/productManagementAdd') +} + +/** 删除按钮操作 */ +// const handleDelete = async (id: number) => { +// try { +// // 删除的二次确认 +// await message.delConfirm() +// // 发起删除 +// await ProductBrandApi.deleteBrand(id) +// message.success(t('common.delSuccess')) +// // 刷新列表 +// await getList() +// } catch {} +// } + +/** 初始化 **/ +onMounted(() => { + // getList() +}) +</script> From ace8f9e302b05b3226d1910290784096092dacc1 Mon Sep 17 00:00:00 2001 From: AhJindeg <AhJindeg@163.com> Date: Mon, 24 Apr 2023 11:50:30 +0800 Subject: [PATCH 08/28] =?UTF-8?q?=F0=9F=90=9E=20fix(styles/index):=20Updat?= =?UTF-8?q?e=20.el-scrollbar=5F=5Fbar=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解决表格内容超过表格总宽度后,横向滚动条前端顶不到表格边缘的问题 --- src/styles/index.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/styles/index.scss b/src/styles/index.scss index 2781c12e..33d29123 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -10,6 +10,12 @@ width: 100% !important; } +// 解决表格内容超过表格总宽度后,横向滚动条前端顶不到表格边缘的问题 +.el-scrollbar__bar { + display: flex; + justify-content: flex-start; +} + /* nprogress 适配 element-plus 的主题色 */ #nprogress { & .bar { From 9680a204ca09cc9febe563fbf9eb6d19f1c1a4ee Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Tue, 25 Apr 2023 21:02:35 +0800 Subject: [PATCH 09/28] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=95=86=E5=93=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/vite/optimize.ts | 17 +++++- src/api/mall/product/category.ts | 12 ++--- .../mall/product/category/CategoryForm.vue | 53 ++++++++----------- src/views/mall/product/category/index.vue | 4 +- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/build/vite/optimize.ts b/build/vite/optimize.ts index 416ea02e..1a7a1b2f 100644 --- a/build/vite/optimize.ts +++ b/build/vite/optimize.ts @@ -64,6 +64,7 @@ const include = [ 'element-plus/es/components/dropdown-menu/style/index', 'element-plus/es/components/dropdown-item/style/index', 'element-plus/es/components/skeleton/style/index', + 'element-plus/es/components/skeleton/style/css', 'element-plus/es/components/backtop/style/css', 'element-plus/es/components/menu/style/css', @@ -76,7 +77,21 @@ const include = [ 'element-plus/es/components/badge/style/css', 'element-plus/es/components/breadcrumb/style/css', 'element-plus/es/components/breadcrumb-item/style/css', - 'element-plus/es/components/image/style/css' + 'element-plus/es/components/image/style/css', + 'element-plus/es/components/tag/style/css', + 'element-plus/es/components/dialog/style/css', + 'element-plus/es/components/form/style/css', + 'element-plus/es/components/form-item/style/css', + 'element-plus/es/components/card/style/css', + 'element-plus/es/components/tooltip/style/css', + 'element-plus/es/components/radio-group/style/css', + 'element-plus/es/components/radio/style/css', + 'element-plus/es/components/input-number/style/css', + 'element-plus/es/components/tree-select/style/css', + 'element-plus/es/components/drawer/style/css', + 'element-plus/es/components/image-viewer/style/css', + 'element-plus/es/components/upload/style/css', + 'element-plus/es/components/switch/style/css' ] const exclude = ['@iconify/json'] diff --git a/src/api/mall/product/category.ts b/src/api/mall/product/category.ts index 7ae81285..8158fc0f 100644 --- a/src/api/mall/product/category.ts +++ b/src/api/mall/product/category.ts @@ -17,17 +17,17 @@ export interface CategoryVO { */ name: string /** - * 分类图片 + * 移动端分类图 */ picUrl: string + /** + * PC 端分类图 + */ + bigPicUrl?: string /** * 分类排序 */ - sort?: number - /** - * 分类描述 - */ - description?: string + sort: number /** * 开启状态 */ diff --git a/src/views/mall/product/category/CategoryForm.vue b/src/views/mall/product/category/CategoryForm.vue index 19bce872..dfe81333 100644 --- a/src/views/mall/product/category/CategoryForm.vue +++ b/src/views/mall/product/category/CategoryForm.vue @@ -4,27 +4,30 @@ ref="formRef" :model="formData" :rules="formRules" - label-width="80px" + label-width="120px" v-loading="formLoading" > <el-form-item label="上级分类" prop="parentId"> - <el-tree-select - v-model="formData.parentId" - :data="categoryTree" - :props="{ label: 'name', value: 'id' }" - :render-after-expand="false" - placeholder="请选择上级分类" - check-strictly - default-expand-all - /> + <el-select v-model="formData.parentId" placeholder="请选择上级分类"> + <el-option :key="0" label="顶级分类" :value="0" /> + <el-option + v-for="item in categoryList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> </el-form-item> <el-form-item label="分类名称" prop="name"> <el-input v-model="formData.name" placeholder="请输入分类名称" /> </el-form-item> - <el-form-item label="分类图片" prop="picUrl"> + <el-form-item label="移动端分类图" prop="picUrl"> <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" /> - <div v-if="formData.parentId === 0" style="font-size: 10px">推荐 200x100 图片分辨率</div> - <div v-else style="font-size: 10px">推荐 100x100 图片分辨率</div> + <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div> + </el-form-item> + <el-form-item label="PC 端分类图" prop="bigPicUrl"> + <UploadImg v-model="formData.bigPicUrl" :limit="1" :is-show-tip="false" /> + <div style="font-size: 10px" class="pl-10px">推荐 468x340 图片分辨率</div> </el-form-item> <el-form-item label="分类排序" prop="sort"> <el-input-number v-model="formData.sort" controls-position="right" :min="0" /> @@ -40,9 +43,6 @@ </el-radio> </el-radio-group> </el-form-item> - <el-form-item label="分类描述"> - <el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" /> - </el-form-item> </el-form> <template #footer> <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> @@ -53,7 +53,6 @@ <script setup lang="ts" name="ProductCategory"> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { CommonStatusEnum } from '@/utils/constants' -import { handleTree } from '@/utils/tree' import * as ProductCategoryApi from '@/api/mall/product/category' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -66,8 +65,8 @@ const formData = ref({ id: undefined, name: '', picUrl: '', - status: CommonStatusEnum.ENABLE, - description: '' + bigPicUrl: '', + status: CommonStatusEnum.ENABLE }) const formRules = reactive({ parentId: [{ required: true, message: '请选择上级分类', trigger: 'blur' }], @@ -77,7 +76,7 @@ const formRules = reactive({ status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }] }) const formRef = ref() // 表单 Ref -const categoryTree = ref<any[]>([]) // 分类树 +const categoryList = ref<any[]>([]) // 分类树 /** 打开弹窗 */ const open = async (type: string, id?: number) => { @@ -95,7 +94,7 @@ const open = async (type: string, id?: number) => { } } // 获得分类树 - await getTree() + categoryList.value = await ProductCategoryApi.getCategoryList({ parentId: 0 }) } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -131,17 +130,9 @@ const resetForm = () => { id: undefined, name: '', picUrl: '', - status: CommonStatusEnum.ENABLE, - description: '' + bigPicUrl: '', + status: CommonStatusEnum.ENABLE } formRef.value?.resetFields() } - -/** 获得分类树 */ -const getTree = async () => { - const data = await ProductCategoryApi.getCategoryList({}) - const tree = handleTree(data, 'id', 'parentId') - const menu = { id: 0, name: '顶级分类', children: tree } - categoryTree.value = [menu] -} </script> diff --git a/src/views/mall/product/category/index.vue b/src/views/mall/product/category/index.vue index f57e35f8..ebe1d63f 100644 --- a/src/views/mall/product/category/index.vue +++ b/src/views/mall/product/category/index.vue @@ -36,9 +36,9 @@ <ContentWrap> <el-table v-loading="loading" :data="list" row-key="id" default-expand-all> <el-table-column label="分类名称" prop="name" sortable /> - <el-table-column label="分类图片" align="center" prop="picUrl"> + <el-table-column label="移动端分类图" align="center" prop="picUrl"> <template #default="scope"> - <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="分类图片" class="h-100px" /> + <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-100px" /> </template> </el-table-column> <el-table-column label="分类排序" align="center" prop="sort" /> From c38abc365c779528d5ea5d2926a895aa1211eaf9 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Wed, 26 Apr 2023 17:17:39 +0800 Subject: [PATCH 10/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=A1=A8=E5=8D=95=E6=A0=A1=E9=AA=8C=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BF=A1=E6=81=AF=E6=8F=90=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=96=B0=E5=BB=BA=E3=80=81=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E3=80=81=E6=8F=90=E4=BA=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/type/index.ts | 22 ++++ src/utils/object.ts | 17 +++ src/views/mall/product/management/addForm.vue | 90 +++++++++++-- .../management/components/BasicInfoForm.vue | 118 +++++++++--------- .../management/components/DescriptionForm.vue | 82 ++++++++++-- .../components/OtherSettingsForm.vue | 87 ++++++++++--- src/views/system/dict/index.vue | 91 ++++++++------ 7 files changed, 374 insertions(+), 133 deletions(-) create mode 100644 src/api/mall/product/management/type/index.ts create mode 100644 src/utils/object.ts diff --git a/src/api/mall/product/management/type/index.ts b/src/api/mall/product/management/type/index.ts new file mode 100644 index 00000000..3d372c45 --- /dev/null +++ b/src/api/mall/product/management/type/index.ts @@ -0,0 +1,22 @@ +export interface SpuType { + name?: string // 商品名称 + categoryId?: number // 商品分类 + keyword?: string // 关键字 + unit?: string // 单位 + picUrl?: string // 商品封面图 + sliderPicUrls?: string[] // 商品轮播图 + introduction?: string // 商品简介 + deliveryTemplateId?: number // 运费模版 + selectRule?: string // 选择规格 TODO 暂时定义 + specType?: boolean // 商品规格 + subCommissionType?: boolean // 分销类型 + description?: string // 商品详情 + sort?: string // 商品排序 + giveIntegral?: number // 赠送积分 + virtualSalesCount?: number // 虚拟销量 + recommendHot?: boolean // 是否热卖 + recommendBenefit?: boolean // 是否优惠 + recommendBest?: boolean // 是否精品 + recommendNew?: boolean // 是否新品 + recommendGood?: boolean // 是否优品 +} diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 00000000..8edd1888 --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,17 @@ +/** + * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2} + * @param target 目标对象 + * @param source 源对象 + */ +export const copyValueToTarget = (target, source) => { + const newObj = Object.assign({}, target, source) + // 删除多余属性 + Object.keys(newObj).forEach((key) => { + // 如果不是target中的属性则删除 + if (Object.keys(target).indexOf(key) === -1) { + delete newObj[key] + } + }) + // 更新目标对象值 + Object.assign(target, newObj) +} diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index d077df5b..f915b204 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -2,13 +2,25 @@ <ContentWrap v-loading="formLoading"> <el-tabs v-model="activeName"> <el-tab-pane label="商品信息" name="basicInfo"> - <BasicInfoForm ref="basicInfoRef" /> + <BasicInfoForm + ref="BasicInfoRef" + v-model:activeName="activeName" + :propFormData="formData" + /> </el-tab-pane> <el-tab-pane label="商品详情" name="description"> - <DescriptionForm ref="DescriptionRef" /> + <DescriptionForm + ref="DescriptionRef" + v-model:activeName="activeName" + :propFormData="formData" + /> </el-tab-pane> <el-tab-pane label="其他设置" name="otherSettings"> - <OtherSettingsForm ref="otherSettingsRef" /> + <OtherSettingsForm + ref="OtherSettingsRef" + v-model:activeName="activeName" + :propFormData="formData" + /> </el-tab-pane> </el-tabs> <el-form> @@ -22,6 +34,7 @@ <script lang="ts" name="ProductManagementForm" setup> import { useTagsViewStore } from '@/store/modules/tagsView' import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' +import { SpuType } from '@/api/mall/product/management/type' // const { t } = useI18n() // 国际化 // const { t } = useI18n() // 国际化 // const message = useMessage() // 消息弹窗 @@ -30,18 +43,77 @@ const { push, currentRoute } = useRouter() // 路由 const { delView } = useTagsViewStore() // 视图操作 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const activeName = ref('otherSettings') // Tag 激活的窗口 -const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() -const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() - +const activeName = ref('basicInfo') // Tag 激活的窗口 +const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Ref +const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref +const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref +const formData = ref<SpuType>({ + name: '', // 商品名称 + categoryId: 0, // 商品分类 + keyword: '', // 关键字 + unit: '', // 单位 + picUrl: '', // 商品封面图 + sliderPicUrls: [], // 商品轮播图 + introduction: '', // 商品简介 + deliveryTemplateId: 0, // 运费模版 + selectRule: '', + specType: false, // 商品规格 + subCommissionType: false, // 分销类型 + description: '', // 商品详情 + sort: 1, // 商品排序 + giveIntegral: 1, // 赠送积分 + virtualSalesCount: 1, // 虚拟销量 + recommendHot: false, // 是否热卖 + recommendBenefit: false, // 是否优惠 + recommendBest: false, // 是否精品 + recommendNew: false, // 是否新品 + recommendGood: false // 是否优品 +}) /** 获得详情 */ const getDetail = async () => {} /** 提交按钮 */ -const submitForm = async () => {} - +const submitForm = async () => { + // TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息 + // 校验各表单 + try { + await unref(BasicInfoRef)?.validate() + await unref(DescriptionRef)?.validate() + await unref(OtherSettingsRef)?.validate() + // 校验都通过后提交表单 + console.log(formData.value) + } catch {} +} +/** + * 重置表单 + */ +const resetForm = async () => { + formData.value = { + name: '', // 商品名称 + categoryId: 0, // 商品分类 + keyword: '', // 关键字 + unit: '', // 单位 + picUrl: '', // 商品封面图 + sliderPicUrls: [], // 商品轮播图 + introduction: '', // 商品简介 + deliveryTemplateId: 0, // 运费模版 + selectRule: '', + specType: false, // 商品规格 + subCommissionType: false, // 分销类型 + description: '', // 商品详情 + sort: 1, // 商品排序 + giveIntegral: 1, // 赠送积分 + virtualSalesCount: 1, // 虚拟销量 + recommendHot: false, // 是否热卖 + recommendBenefit: false, // 是否优惠 + recommendBest: false, // 是否精品 + recommendNew: false, // 是否新品 + recommendGood: false // 是否优品 + } +} /** 关闭按钮 */ const close = () => { + resetForm() delView(unref(currentRoute)) push('/product/product-management') } diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index 1b33e9eb..8074e67b 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -1,5 +1,5 @@ <template> - <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px"> + <el-form ref="ProductManagementBasicInfoRef" :model="formData" :rules="rules" label-width="120px"> <el-row> <el-col :span="12"> <el-form-item label="商品名称" prop="name"> @@ -40,26 +40,12 @@ </el-col> <el-col :span="12"> <el-form-item label="商品封面图" prop="picUrl"> - <div class="demo-image__preview pt-5px pb-5px pl-11x pr-11px"> - <el-image - :initial-index="0" - :preview-src-list="srcList" - :src="url" - :zoom-rate="1.2" - fit="cover" - style="width: 100%; height: 90px" - /> - </div> + <UploadImg v-model="formData.picUrl" height="80px" /> </el-form-item> </el-col> <el-col :span="24"> <el-form-item label="商品轮播图" prop="sliderPicUrls"> - <el-button>添加轮播图</el-button> - <el-carousel :interval="3000" height="200px" style="width: 100%" type="card"> - <el-carousel-item v-for="item in 6" :key="item"> - <h3 justify="center" text="2xl">{{ item }}</h3> - </el-carousel-item> - </el-carousel> + <UploadImgs v-model="formData.sliderPicUrls" /> </el-form-item> </el-col> <el-col :span="12"> @@ -72,6 +58,7 @@ <el-col :span="12"> <el-button class="ml-20px">运费模板</el-button> </el-col> + <!-- TODO 商品规格和分销类型切换待定 --> <el-col :span="12"> <el-form-item label="商品规格" props="specType"> <el-radio-group v-model="formData.specType" @change="changeSpecType(formData.specType)"> @@ -113,23 +100,31 @@ </el-form> </template> <script lang="ts" name="ProductManagementBasicInfoForm" setup> -// TODO 商品封面测试数据 +import { PropType } from 'vue' import { defaultProps } from '@/utils/tree' +import type { SpuType } from '@/api/mall/product/management/type' +import { UploadImg, UploadImgs } from '@/components/UploadFile' +import { copyValueToTarget } from '@/utils/object' -const url = 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg' -const srcList = ['https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'] +const message = useMessage() // 消息弹窗 +const props = defineProps({ + propFormData: { + type: Object as PropType<SpuType>, + default: () => {} + } +}) -const formRef = ref() -const formData = ref({ +const ProductManagementBasicInfoRef = ref() // 表单Ref +const formData = ref<SpuType>({ name: '', // 商品名称 - categoryId: '', // 商品分类 + categoryId: 155415, // 商品分类 keyword: '', // 关键字 unit: '', // 单位 picUrl: '', // 商品封面图 sliderPicUrls: [], // 商品轮播图 introduction: '', // 商品简介 deliveryTemplateId: '', // 运费模版 - selectRule: '', + selectRule: '', // 选择规则 TODO 暂定 specType: false, // 商品规格 subCommissionType: false // 分销类型 }) @@ -138,12 +133,47 @@ const rules = reactive({ categoryId: [required], keyword: [required], unit: [required], + introduction: [required], picUrl: [required], - sliderPicUrls: [required], - deliveryTemplateId: [required], - specType: [required], - subCommissionType: [required] + sliderPicUrls: [required] + // deliveryTemplateId: [required], + // specType: [required], + // subCommissionType: [required], }) +/** + * 将传进来的值赋值给formData + */ +watch( + () => props.propFormData, + (data) => { + if (!data) return + copyValueToTarget(formData.value, data) + }, + { + deep: true, + immediate: true + } +) +const emit = defineEmits(['update:activeName']) +/** + * 表单校验 + */ +const validate = async () => { + // 校验表单 + if (!ProductManagementBasicInfoRef) return + return await unref(ProductManagementBasicInfoRef).validate((valid) => { + if (!valid) { + message.warning('商品信息未完善!!') + emit('update:activeName', 'basicInfo') + // 目的截断之后的校验 + throw new Error('商品信息未完善!!') + } else { + // 校验通过更新数据 + Object.assign(props.propFormData, formData.value) + } + }) +} +defineExpose({ validate }) // 选择规格 const changeSpecType = (specType) => { console.log(specType) @@ -157,35 +187,3 @@ const confirm = () => {} // 添加规格 const addRule = () => {} </script> -<style scoped> -/*TODO 商品轮播图测试样式*/ -.el-carousel__item h3 { - color: #475669; - opacity: 0.75; - line-height: 200px; - margin: 0; - text-align: center; -} - -.el-carousel__item:nth-child(2n) { - background-color: #99a9bf; -} - -.el-carousel__item:nth-child(2n + 1) { - background-color: #d3dce6; -} - -/*TODO 商品封面测试样式*/ -.demo-image__error .image-slot { - font-size: 30px; -} - -.demo-image__error .image-slot .el-icon { - font-size: 30px; -} - -.demo-image__error .el-image { - width: 100%; - height: 200px; -} -</style> diff --git a/src/views/mall/product/management/components/DescriptionForm.vue b/src/views/mall/product/management/components/DescriptionForm.vue index 53609705..f29f29b4 100644 --- a/src/views/mall/product/management/components/DescriptionForm.vue +++ b/src/views/mall/product/management/components/DescriptionForm.vue @@ -1,13 +1,79 @@ <template> - <!--富文本编辑器组件--> - <el-row> - <Editor v-model="content" :editor-config="editorConfig" /> - </el-row> + <el-form ref="DescriptionFormRef" :model="formData" :rules="rules" label-width="120px"> + <!--富文本编辑器组件--> + <el-form-item label="商品详情" prop="description"> + <Editor v-model:modelValue="formData.description" /> + </el-form-item> + </el-form> </template> <script lang="ts" name="DescriptionForm" setup> +import type { SpuType } from '@/api/mall/product/management/type' import { Editor } from '@/components/Editor' -import { createEditorConfig } from '@/views/mp/draft/editor-config' -// TODO 模拟参数 -const content = ref('') -const editorConfig = createEditorConfig('', 1) +import { PropType } from 'vue' +import { copyValueToTarget } from '@/utils/object' + +const message = useMessage() // 消息弹窗 +const props = defineProps({ + propFormData: { + type: Object as PropType<SpuType>, + default: () => {} + } +}) +const DescriptionFormRef = ref() // 表单Ref +const formData = ref<SpuType>({ + description: '' // 商品详情 +}) +/** + * 富文本编辑器如果输入过再清空会有残留,需再重置一次 + */ +watch( + () => formData.value.description, + (newValue) => { + if ('<p><br></p>' === newValue) { + formData.value.description = '' + } + }, + { + deep: true, + immediate: true + } +) +// 表单规则 +const rules = reactive({ + description: [required] +}) +/** + * 将传进来的值赋值给formData + */ +watch( + () => props.propFormData, + (data) => { + if (!data) return + copyValueToTarget(formData.value, data) + }, + { + deep: true, + immediate: true + } +) +const emit = defineEmits(['update:activeName']) +/** + * 表单校验 + */ +const validate = async () => { + // 校验表单 + if (!DescriptionFormRef) return + return unref(DescriptionFormRef).validate((valid) => { + if (!valid) { + message.warning('商品详情为完善!!') + emit('update:activeName', 'description') + // 目的截断之后的校验 + throw new Error('商品详情为完善!!') + } else { + // 校验通过更新数据 + Object.assign(props.propFormData, formData.value) + } + }) +} +defineExpose({ validate }) </script> diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue index e8152883..155a0aba 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -1,19 +1,19 @@ <template> - <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px"> + <el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px"> <el-row> <el-col :span="24"> <el-col :span="8"> - <el-form-item label="商品排序"> + <el-form-item label="商品排序" prop="sort"> <el-input-number v-model="formData.sort" :min="0" /> </el-form-item> </el-col> <el-col :span="8"> - <el-form-item label="赠送积分"> + <el-form-item label="赠送积分" prop="giveIntegral"> <el-input-number v-model="formData.giveIntegral" :min="0" /> </el-form-item> </el-col> <el-col :span="8"> - <el-form-item label="虚拟销量"> + <el-form-item label="虚拟销量" prop="virtualSalesCount"> <el-input-number v-model="formData.virtualSalesCount" :min="0" @@ -50,6 +50,18 @@ </template> <script lang="ts" name="OtherSettingsForm" setup> // 商品推荐 +import type { SpuType } from '@/api/mall/product/management/type' +import { PropType } from 'vue' +import { copyValueToTarget } from '@/utils/object' + +const message = useMessage() // 消息弹窗 +const props = defineProps({ + propFormData: { + type: Object as PropType<SpuType>, + default: () => {} + } +}) +// 商品推荐选项 const recommend = [ { name: '是否热卖', value: 'recommendHot' }, { name: '是否优惠', value: 'recommendBenefit' }, @@ -57,7 +69,9 @@ const recommend = [ { name: '是否新品', value: 'recommendNew' }, { name: '是否优品', value: 'recommendGood' } ] -const checkboxGroup = ref<string[]>([]) +// 选中推荐选项 +const checkboxGroup = ref<string[]>(['recommendHot']) +// 选择商品后赋值 const onChangeGroup = () => { checkboxGroup.value.includes('recommendHot') ? (formData.value.recommendHot = true) @@ -75,20 +89,63 @@ const onChangeGroup = () => { ? (formData.value.recommendGood = true) : (formData.value.recommendGood = false) } -const formRef = ref() -const formData = ref({ - sort: '', - giveIntegral: 666, - virtualSalesCount: 565656, - recommendHot: false, - recommendBenefit: false, - recommendBest: false, - recommendNew: false, - recommendGood: false +const OtherSettingsFormRef = ref() // 表单Ref +// 表单数据 +const formData = ref<SpuType>({ + sort: 12, // 商品排序 + giveIntegral: 666, // 赠送积分 + virtualSalesCount: 565656, // 虚拟销量 + recommendHot: false, // 是否热卖 + recommendBenefit: false, // 是否优惠 + recommendBest: false, // 是否精品 + recommendNew: false, // 是否新品 + recommendGood: false // 是否优品 }) +// 表单规则 const rules = reactive({ sort: [required], giveIntegral: [required], virtualSalesCount: [required] }) +/** + * 将传进来的值赋值给formData + */ +watch( + () => props.propFormData, + (data) => { + if (!data) return + copyValueToTarget(formData.value, data) + checkboxGroup.value = [] + formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : '' + formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : '' + formData.value.recommendBest ? checkboxGroup.value.push('recommendBest') : '' + formData.value.recommendNew ? checkboxGroup.value.push('recommendNew') : '' + formData.value.recommendGood ? checkboxGroup.value.push('recommendGood') : '' + }, + { + deep: true, + immediate: true + } +) +const emit = defineEmits(['update:activeName']) +/** + * 表单校验 + */ +const validate = async () => { + // 校验表单 + if (!OtherSettingsFormRef) return + return await unref(OtherSettingsFormRef).validate((valid) => { + if (!valid) { + message.warning('商品其他设置未完善!!') + emit('update:activeName', 'otherSettings') + // 目的截断之后的校验 + throw new Error('商品其他设置未完善!!') + } else { + // 校验通过更新数据 + Object.assign(props.propFormData, formData.value) + } + }) +} + +defineExpose({ validate }) </script> diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue index bbcd8a2c..755b9415 100644 --- a/src/views/system/dict/index.vue +++ b/src/views/system/dict/index.vue @@ -2,36 +2,36 @@ <!-- 搜索工作栏 --> <ContentWrap> <el-form - class="-mb-15px" - :model="queryParams" ref="queryFormRef" :inline="true" + :model="queryParams" + class="-mb-15px" label-width="68px" > <el-form-item label="字典名称" prop="name"> <el-input v-model="queryParams.name" - placeholder="请输入字典名称" - clearable - @keyup.enter="handleQuery" class="!w-240px" + clearable + placeholder="请输入字典名称" + @keyup.enter="handleQuery" /> </el-form-item> <el-form-item label="字典类型" prop="type"> <el-input v-model="queryParams.type" - placeholder="请输入字典类型" - clearable - @keyup.enter="handleQuery" class="!w-240px" + clearable + placeholder="请输入字典类型" + @keyup.enter="handleQuery" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" - placeholder="请选择字典状态" - clearable class="!w-240px" + clearable + placeholder="请选择字典状态" > <el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" @@ -44,33 +44,41 @@ <el-form-item label="创建时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" - value-format="yyyy-MM-dd HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" + end-placeholder="结束日期" + start-placeholder="开始日期" + type="daterange" + value-format="yyyy-MM-dd HH:mm:ss" /> </el-form-item> <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['system:dict:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 </el-button> <el-button - type="success" + v-hasPermi="['system:dict:create']" plain - @click="handleExport" - :loading="exportLoading" - v-hasPermi="['system:dict:export']" + type="primary" + @click="openForm('create')" > - <Icon icon="ep:download" class="mr-5px" /> 导出 + <Icon class="mr-5px" icon="ep:plus" /> + 新增 + </el-button> + <el-button + v-hasPermi="['system:dict:export']" + :loading="exportLoading" + plain + type="success" + @click="handleExport" + > + <Icon class="mr-5px" icon="ep:download" /> + 导出 </el-button> </el-form-item> </el-form> @@ -79,29 +87,29 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="字典编号" align="center" prop="id" /> - <el-table-column label="字典名称" align="center" prop="name" show-overflow-tooltip /> - <el-table-column label="字典类型" align="center" prop="type" width="300" /> - <el-table-column label="状态" align="center" prop="status"> + <el-table-column align="center" label="字典编号" prop="id" /> + <el-table-column align="center" label="字典名称" prop="name" show-overflow-tooltip /> + <el-table-column align="center" label="字典类型" prop="type" width="300" /> + <el-table-column align="center" label="状态" prop="status"> <template #default="scope"> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> </template> </el-table-column> - <el-table-column label="备注" align="center" prop="remark" /> + <el-table-column align="center" label="备注" prop="remark" /> <el-table-column - label="创建时间" :formatter="dateFormatter" align="center" + label="创建时间" prop="createTime" width="180" /> - <el-table-column label="操作" align="center"> + <el-table-column align="center" label="操作"> <template #default="scope"> <el-button + v-hasPermi="['system:dict:update']" link type="primary" @click="openForm('update', scope.row.id)" - v-hasPermi="['system:dict:update']" > 修改 </el-button> @@ -109,10 +117,10 @@ <el-button link type="primary">数据</el-button> </router-link> <el-button + v-hasPermi="['system:dict:delete']" link type="danger" @click="handleDelete(scope.row.id)" - v-hasPermi="['system:dict:delete']" > 删除 </el-button> @@ -121,9 +129,9 @@ </el-table> <!-- 分页 --> <Pagination - :total="total" - v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" @pagination="getList" /> </ContentWrap> @@ -132,12 +140,13 @@ <DictTypeForm ref="formRef" @success="getList" /> </template> -<script setup lang="ts" name="SystemDictType"> -import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' +<script lang="ts" name="SystemDictType" setup> +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' import * as DictTypeApi from '@/api/system/dict/dict.type' import DictTypeForm from './DictTypeForm.vue' import download from '@/utils/download' + const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 From ab1685a74100243797611c0a4e2c2f6cb9a6380b Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Wed, 26 Apr 2023 17:53:17 +0800 Subject: [PATCH 11/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=80=89=E6=8B=A9=E5=95=86=E5=93=81=E5=88=86?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/type/index.ts | 2 +- src/views/mall/product/management/addForm.vue | 2 +- .../product/management/components/BasicInfoForm.vue | 13 ++++++++++--- .../management/components/OtherSettingsForm.vue | 6 +++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/api/mall/product/management/type/index.ts b/src/api/mall/product/management/type/index.ts index 3d372c45..a1224d48 100644 --- a/src/api/mall/product/management/type/index.ts +++ b/src/api/mall/product/management/type/index.ts @@ -1,6 +1,6 @@ export interface SpuType { name?: string // 商品名称 - categoryId?: number // 商品分类 + categoryId?: number | undefined // 商品分类 keyword?: string // 关键字 unit?: string // 单位 picUrl?: string // 商品封面图 diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index f915b204..9ef24f69 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -49,7 +49,7 @@ const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详 const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref const formData = ref<SpuType>({ name: '', // 商品名称 - categoryId: 0, // 商品分类 + categoryId: undefined, // 商品分类 keyword: '', // 关键字 unit: '', // 单位 picUrl: '', // 商品封面图 diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index 8074e67b..94fe1e10 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -10,7 +10,7 @@ <el-form-item label="商品分类" prop="categoryId"> <el-tree-select v-model="formData.categoryId" - :data="[]" + :data="categoryList" :props="defaultProps" check-strictly node-key="id" @@ -101,10 +101,11 @@ </template> <script lang="ts" name="ProductManagementBasicInfoForm" setup> import { PropType } from 'vue' -import { defaultProps } from '@/utils/tree' import type { SpuType } from '@/api/mall/product/management/type' import { UploadImg, UploadImgs } from '@/components/UploadFile' import { copyValueToTarget } from '@/utils/object' +import * as ProductCategoryApi from '@/api/mall/product/category' +import { defaultProps, handleTree } from '@/utils/tree' const message = useMessage() // 消息弹窗 const props = defineProps({ @@ -117,7 +118,7 @@ const props = defineProps({ const ProductManagementBasicInfoRef = ref() // 表单Ref const formData = ref<SpuType>({ name: '', // 商品名称 - categoryId: 155415, // 商品分类 + categoryId: undefined, // 商品分类 keyword: '', // 关键字 unit: '', // 单位 picUrl: '', // 商品封面图 @@ -186,4 +187,10 @@ const changeSubCommissionType = (subCommissionType) => { const confirm = () => {} // 添加规格 const addRule = () => {} +const categoryList = ref() // 分类树 +onMounted(async () => { + // 获得分类树 + const data = await ProductCategoryApi.getCategoryList({}) + categoryList.value = handleTree(data, 'id', 'parentId') +}) </script> diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue index 155a0aba..63fdb22a 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -92,9 +92,9 @@ const onChangeGroup = () => { const OtherSettingsFormRef = ref() // 表单Ref // 表单数据 const formData = ref<SpuType>({ - sort: 12, // 商品排序 - giveIntegral: 666, // 赠送积分 - virtualSalesCount: 565656, // 虚拟销量 + sort: 1, // 商品排序 + giveIntegral: 1, // 赠送积分 + virtualSalesCount: 1, // 虚拟销量 recommendHot: false, // 是否热卖 recommendBenefit: false, // 是否优惠 recommendBest: false, // 是否精品 From 61218ae71111acff6ba9697314c91963ece5b95f Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sat, 29 Apr 2023 21:39:25 +0800 Subject: [PATCH 12/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90=E7=9B=B8=E5=85=B3=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/sku.ts | 0 src/api/mall/product/management/spu.ts | 15 +++ .../mall/product/management/type/skuType.ts | 75 ++++++++++++ .../management/type/{index.ts => spuType.ts} | 3 + src/views/mall/product/management/addForm.vue | 34 ++++-- .../management/components/BasicInfoForm.vue | 109 +++++++++++++----- .../management/components/DescriptionForm.vue | 2 +- .../components/OtherSettingsForm.vue | 2 +- .../components/ProductAttributes.vue | 82 +++++++++++++ .../components/ProductAttributesAddForm.vue | 82 +++++++++++++ .../management/components/SkuList/index.vue | 86 ++++++++++++++ src/views/mall/product/management/index.vue | 22 ++-- src/views/mall/product/property/index.vue | 56 +++++---- 13 files changed, 492 insertions(+), 76 deletions(-) create mode 100644 src/api/mall/product/management/sku.ts create mode 100644 src/api/mall/product/management/spu.ts create mode 100644 src/api/mall/product/management/type/skuType.ts rename src/api/mall/product/management/type/{index.ts => spuType.ts} (92%) create mode 100644 src/views/mall/product/management/components/ProductAttributes.vue create mode 100644 src/views/mall/product/management/components/ProductAttributesAddForm.vue create mode 100644 src/views/mall/product/management/components/SkuList/index.vue diff --git a/src/api/mall/product/management/sku.ts b/src/api/mall/product/management/sku.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/api/mall/product/management/spu.ts b/src/api/mall/product/management/spu.ts new file mode 100644 index 00000000..d5bf52ee --- /dev/null +++ b/src/api/mall/product/management/spu.ts @@ -0,0 +1,15 @@ +import request from '@/config/axios' +import type { SpuType } from './type/spuType' + +// 获得sku列表 +export const getSkuList = (params: any) => { + return request.get({ url: '/product/sku/list', params }) +} +// 创建商品spu +export const createSpu = (data: SpuType) => { + return request.post({ url: '/product/spu/create', data }) +} +// 更新商品spu +export const updateSpu = (data: SpuType) => { + return request.put({ url: '/product/spu/update', data }) +} diff --git a/src/api/mall/product/management/type/skuType.ts b/src/api/mall/product/management/type/skuType.ts new file mode 100644 index 00000000..6de0d893 --- /dev/null +++ b/src/api/mall/product/management/type/skuType.ts @@ -0,0 +1,75 @@ +export interface Property { + /** + * 属性编号 + * + * 关联 {@link ProductPropertyDO#getId()} + */ + propertyId?: number + /** + * 属性值编号 + * + * 关联 {@link ProductPropertyValueDO#getId()} + */ + valueId?: number +} + +export interface SkuType { + /** + * 商品 SKU 编号,自增 + */ + id?: number + /** + * SPU 编号 + */ + spuId?: number + /** + * 属性数组,JSON 格式 + */ + properties?: Property[] + /** + * 商品价格,单位:分 + */ + price?: number + /** + * 市场价,单位:分 + */ + marketPrice?: number + /** + * 成本价,单位:分 + */ + costPrice?: number + /** + * 商品条码 + */ + barCode?: string + /** + * 图片地址 + */ + picUrl?: string + /** + * 库存 + */ + stock?: number + /** + * 商品重量,单位:kg 千克 + */ + weight?: number + /** + * 商品体积,单位:m^3 平米 + */ + volume?: number + + /** + * 一级分销的佣金,单位:分 + */ + subCommissionFirstPrice?: number + /** + * 二级分销的佣金,单位:分 + */ + subCommissionSecondPrice?: number + + /** + * 商品销量 + */ + salesCount?: number +} diff --git a/src/api/mall/product/management/type/index.ts b/src/api/mall/product/management/type/spuType.ts similarity index 92% rename from src/api/mall/product/management/type/index.ts rename to src/api/mall/product/management/type/spuType.ts index a1224d48..f51bc526 100644 --- a/src/api/mall/product/management/type/index.ts +++ b/src/api/mall/product/management/type/spuType.ts @@ -1,3 +1,5 @@ +import { SkuType } from './skuType' + export interface SpuType { name?: string // 商品名称 categoryId?: number | undefined // 商品分类 @@ -10,6 +12,7 @@ export interface SpuType { selectRule?: string // 选择规格 TODO 暂时定义 specType?: boolean // 商品规格 subCommissionType?: boolean // 分销类型 + skus?: SkuType[] // sku数组 description?: string // 商品详情 sort?: string // 商品排序 giveIntegral?: number // 赠送积分 diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index 9ef24f69..ad973394 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -34,12 +34,14 @@ <script lang="ts" name="ProductManagementForm" setup> import { useTagsViewStore } from '@/store/modules/tagsView' import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' -import { SpuType } from '@/api/mall/product/management/type' // const { t } = useI18n() // 国际化 +import type { SpuType } from '@/api/mall/product/management/type/spuType' +// 业务api +import * as managementApi from '@/api/mall/product/management/spu' -// const { t } = useI18n() // 国际化 -// const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 const { push, currentRoute } = useRouter() // 路由 -// const { query } = useRoute() // 查询参数 +const { query } = useRoute() // 查询参数 const { delView } = useTagsViewStore() // 视图操作 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 @@ -70,10 +72,17 @@ const formData = ref<SpuType>({ recommendGood: false // 是否优品 }) /** 获得详情 */ -const getDetail = async () => {} +const getDetail = async () => { + const id = query.id as unknown as number + if (!id) { + return + } +} /** 提交按钮 */ const submitForm = async () => { + // 提交请求 + formLoading.value = true // TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息 // 校验各表单 try { @@ -81,9 +90,20 @@ const submitForm = async () => { await unref(DescriptionRef)?.validate() await unref(OtherSettingsRef)?.validate() // 校验都通过后提交表单 - console.log(formData.value) - } catch {} + const data = formData.value as SpuType + const id = query.id as unknown as number + if (!id) { + await managementApi.createSpu(data) + message.success(t('common.createSuccess')) + } else { + await managementApi.updateSpu(data) + message.success(t('common.updateSuccess')) + } + } finally { + formLoading.value = false + } } + /** * 重置表单 */ diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index 94fe1e10..cd4d5c90 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -58,7 +58,6 @@ <el-col :span="12"> <el-button class="ml-20px">运费模板</el-button> </el-col> - <!-- TODO 商品规格和分销类型切换待定 --> <el-col :span="12"> <el-form-item label="商品规格" props="specType"> <el-radio-group v-model="formData.specType" @change="changeSpecType(formData.specType)"> @@ -67,45 +66,41 @@ </el-radio-group> </el-form-item> </el-col> - <!-- TODO 商品规格和分销类型切换待定 --> <el-col :span="12"> <el-form-item label="分销类型" props="subCommissionType"> - <el-radio-group - v-model="formData.subCommissionType" - @change="changeSubCommissionType(formData.subCommissionType)" - > + <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType"> <el-radio :label="false">默认设置</el-radio> <el-radio :label="true" class="radio">自行设置</el-radio> </el-radio-group> </el-form-item> </el-col> <!-- 多规格添加--> - <el-col v-if="formData.specType" :span="24"> - <el-form-item label="选择规格" prop=""> - <div class="acea-row"> - <el-select v-model="formData.selectRule"> - <el-option - v-for="item in []" - :key="item.id" - :label="item.ruleName" - :value="item.id" - /> - </el-select> - <el-button class="mr-20px" type="primary" @click="confirm">确认</el-button> - <el-button class="mr-15px" @click="addRule">添加规格</el-button> - </div> + <el-col :span="24"> + <el-form-item v-if="formData.specType" label="商品属性" prop=""> + <el-button class="mr-15px" @click="AttributesAddFormRef.open()">添加规格</el-button> + <ProductAttributes :attribute-data="attributeList" /> + </el-form-item> + <el-form-item> + <SkuList :sku-data="formData.skus" :subCommissionType="formData.subCommissionType" /> </el-form-item> </el-col> </el-row> </el-form> + <ProductAttributesAddForm ref="AttributesAddFormRef" @success="addAttribute" /> </template> <script lang="ts" name="ProductManagementBasicInfoForm" setup> import { PropType } from 'vue' -import type { SpuType } from '@/api/mall/product/management/type' +import type { SpuType } from '@/api/mall/product/management/type/spuType' import { UploadImg, UploadImgs } from '@/components/UploadFile' +import SkuList from './SkuList/index.vue' +import ProductAttributesAddForm from './ProductAttributesAddForm.vue' +import ProductAttributes from './ProductAttributes.vue' import { copyValueToTarget } from '@/utils/object' +// 业务Api import * as ProductCategoryApi from '@/api/mall/product/category' +import * as PropertyApi from '@/api/mall/product/property' import { defaultProps, handleTree } from '@/utils/tree' +import { ElInput } from 'element-plus' const message = useMessage() // 消息弹窗 const props = defineProps({ @@ -114,9 +109,21 @@ const props = defineProps({ default: () => {} } }) - +const AttributesAddFormRef = ref() // 添加商品属性表单 const ProductManagementBasicInfoRef = ref() // 表单Ref -const formData = ref<SpuType>({ +// 属性列表 +const attributeList = ref([ + { + id: 1, + name: '颜色', + attributeValues: [{ id: 1, name: '白色' }] + } +]) +const addAttribute = async (propertyId: number) => { + const data = await PropertyApi.getPropertyValuePage({ id: propertyId }) + console.log(data) +} +const formData = reactive<SpuType>({ name: '', // 商品名称 categoryId: undefined, // 商品分类 keyword: '', // 关键字 @@ -124,10 +131,46 @@ const formData = ref<SpuType>({ picUrl: '', // 商品封面图 sliderPicUrls: [], // 商品轮播图 introduction: '', // 商品简介 - deliveryTemplateId: '', // 运费模版 + deliveryTemplateId: 1, // 运费模版 selectRule: '', // 选择规则 TODO 暂定 specType: false, // 商品规格 - subCommissionType: false // 分销类型 + subCommissionType: false, // 分销类型 + skus: [ + { + /** + * 商品价格,单位:分 + */ + price: 0, + /** + * 市场价,单位:分 + */ + marketPrice: 0, + /** + * 成本价,单位:分 + */ + costPrice: 0, + /** + * 商品条码 + */ + barCode: '', + /** + * 图片地址 + */ + picUrl: '', + /** + * 库存 + */ + stock: 0, + /** + * 商品重量,单位:kg 千克 + */ + weight: 0, + /** + * 商品体积,单位:m^3 平米 + */ + volume: 0 + } + ] }) const rules = reactive({ name: [required], @@ -148,7 +191,7 @@ watch( () => props.propFormData, (data) => { if (!data) return - copyValueToTarget(formData.value, data) + copyValueToTarget(formData, data) }, { deep: true, @@ -170,7 +213,7 @@ const validate = async () => { throw new Error('商品信息未完善!!') } else { // 校验通过更新数据 - Object.assign(props.propFormData, formData.value) + Object.assign(props.propFormData, formData) } }) } @@ -180,13 +223,17 @@ const changeSpecType = (specType) => { console.log(specType) } // 分销类型 -const changeSubCommissionType = (subCommissionType) => { - console.log(subCommissionType) +const changeSubCommissionType = () => { + // 默认为零,类型切换后也要重置为零 + for (const item of formData.skus) { + item.subCommissionFirstPrice = 0 + item.subCommissionSecondPrice = 0 + } } // 选择属性确认 -const confirm = () => {} +// const confirm = () => {} // 添加规格 -const addRule = () => {} +// const addRule = () => {} const categoryList = ref() // 分类树 onMounted(async () => { // 获得分类树 diff --git a/src/views/mall/product/management/components/DescriptionForm.vue b/src/views/mall/product/management/components/DescriptionForm.vue index f29f29b4..541ff6b5 100644 --- a/src/views/mall/product/management/components/DescriptionForm.vue +++ b/src/views/mall/product/management/components/DescriptionForm.vue @@ -7,7 +7,7 @@ </el-form> </template> <script lang="ts" name="DescriptionForm" setup> -import type { SpuType } from '@/api/mall/product/management/type' +import type { SpuType } from '@/api/mall/product/management/type/spuType' import { Editor } from '@/components/Editor' import { PropType } from 'vue' import { copyValueToTarget } from '@/utils/object' diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue index 63fdb22a..106eb748 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -50,7 +50,7 @@ </template> <script lang="ts" name="OtherSettingsForm" setup> // 商品推荐 -import type { SpuType } from '@/api/mall/product/management/type' +import type { SpuType } from '@/api/mall/product/management/type/spuType' import { PropType } from 'vue' import { copyValueToTarget } from '@/utils/object' diff --git a/src/views/mall/product/management/components/ProductAttributes.vue b/src/views/mall/product/management/components/ProductAttributes.vue new file mode 100644 index 00000000..95cf67b7 --- /dev/null +++ b/src/views/mall/product/management/components/ProductAttributes.vue @@ -0,0 +1,82 @@ +<template> + <el-col v-for="(item, index) in attributeList" :key="index"> + <div> + <el-text class="mx-1">属性名:</el-text> + <el-text class="mx-1">{{ item.name }}</el-text> + </div> + <div> + <el-text class="mx-1">属性值:</el-text> + <el-tag + v-for="(value, valueIndex) in item.attributeValues" + :key="value.name" + :disable-transitions="false" + class="mx-1" + closable + @close="handleClose(index, valueIndex)" + > + {{ value.name }} + </el-tag> + <el-input + v-if="inputVisible" + ref="InputRef" + v-model="inputValue" + class="!w-20" + size="small" + @blur="handleInputConfirm(index)" + @keyup.enter="handleInputConfirm(index)" + /> + <el-button v-else class="button-new-tag ml-1" size="small" @click="showInput(index)"> + + 添加 + </el-button> + </div> + <el-divider class="my-10px" /> + </el-col> +</template> + +<script lang="ts" name="ProductAttributes" setup> +import { ElInput } from 'element-plus' + +const inputValue = ref('') // 输入框值 +const inputVisible = ref(false) // 输入框显隐控制 +const InputRef = ref<InstanceType<typeof ElInput>>() //标签输入框Ref +const attributeList = ref([]) +const props = defineProps({ + attributeData: { + type: Object, + default: () => {} + } +}) + +watch( + () => props.attributeData, + (data) => { + if (!data) return + attributeList.value = data + }, + { + deep: true, + immediate: true + } +) +/** 删除标签 tagValue 标签值*/ +const handleClose = (index, valueIndex) => { + const av = attributeList.value[index].attributeValues + av.splice(valueIndex, 1) +} +/** 显示输入框并获取焦点 */ +const showInput = (index) => { + inputVisible.value = true + nextTick(() => { + InputRef.value[index]!.input!.focus() + }) +} +/** 输入框失去焦点或点击回车时触发 */ +const handleInputConfirm = (index) => { + if (inputValue.value) { + // 因为ref再循环里,所以需要index获取对应的ref + attributeList.value[index].attributeValues.push({ name: inputValue.value }) + } + inputVisible.value = false + inputValue.value = '' +} +</script> diff --git a/src/views/mall/product/management/components/ProductAttributesAddForm.vue b/src/views/mall/product/management/components/ProductAttributesAddForm.vue new file mode 100644 index 00000000..70fd2824 --- /dev/null +++ b/src/views/mall/product/management/components/ProductAttributesAddForm.vue @@ -0,0 +1,82 @@ +<template> + <Dialog v-model="dialogVisible" :title="dialogTitle"> + <el-form + ref="formRef" + v-loading="formLoading" + :model="formData" + :rules="formRules" + label-width="80px" + > + <el-form-item label="名称" prop="name"> + <el-input v-model="formData.name" placeholder="请输入名称" /> + </el-form-item> + <el-form-item label="备注" prop="remark"> + <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" /> + </el-form-item> + </el-form> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" name="ProductPropertyForm" setup> +import * as PropertyApi from '@/api/mall/product/property' + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('添加商品属性') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref({ + name: '', + remark: '' +}) +const formRules = reactive({ + name: [{ required: true, message: '名称不能为空', trigger: 'blur' }] +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async () => { + dialogVisible.value = true + resetForm() +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + if (!formRef) return + const valid = await formRef.value.validate() + if (!valid) return + // 提交请求 + formLoading.value = true + try { + const data = formData.value as PropertyApi.PropertyVO + // 检查属性是否已存在,如果有则返回属性和其下属性值 + const res = await PropertyApi.getPropertyListAndValue({ name: data.name }) + if (res.length === 0) { + const propertyId = await PropertyApi.createProperty(data) + emit('success', { id: propertyId, ...formData.value, values: [] }) + } else { + emit(res[0]) // 因为只用一个 + } + message.success(t('common.createSuccess')) + dialogVisible.value = false + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + name: '', + remark: '' + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/mall/product/management/components/SkuList/index.vue b/src/views/mall/product/management/components/SkuList/index.vue new file mode 100644 index 00000000..fd148126 --- /dev/null +++ b/src/views/mall/product/management/components/SkuList/index.vue @@ -0,0 +1,86 @@ +<template> + <el-table :data="SkuData" border class="tabNumWidth" size="small"> + <el-table-column align="center" fixed="left" label="图片" min-width="100"> + <template #default="{ row }"> + <UploadImg v-model="row.picUrl" height="80px" width="100%" /> + </template> + </el-table-column> + <el-table-column align="center" label="商品条码" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.barCode" :min="0" class="w-100%" /> + </template> + </el-table-column> + <el-table-column align="center" label="销售价(分)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.price" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="市场价(分)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.marketPrice" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="成本价(分)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.costPrice" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="库存" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.stock" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="重量(kg)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.weight" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="体积(m^3)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.volume" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <template v-if="subCommissionType"> + <el-table-column align="center" label="一级返佣(分)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.subCommissionFirstPrice" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + <el-table-column align="center" label="二级返佣(分)" min-width="120"> + <template #default="{ row }"> + <el-input v-model="row.subCommissionSecondPrice" :min="0" class="w-100%" type="number" /> + </template> + </el-table-column> + </template> + </el-table> +</template> + +<script lang="ts" name="index" setup> +import { propTypes } from '@/utils/propTypes' +import { UploadImg } from '@/components/UploadFile' +import { PropType } from 'vue' +import type { SkuType } from '@/api/mall/product/management/type/skuType' + +const props = defineProps({ + skuData: { + type: Array as PropType<SkuType>, + default: () => [] + }, + subCommissionType: propTypes.bool.def(false) // 分销类型 +}) +const SkuData = ref<SkuType[]>([]) +/** + * 将传进来的值赋值给SkuData + */ +watch( + () => props.skuData, + (data) => { + if (!data) return + SkuData.value = data + }, + { + deep: true, + immediate: true + } +) +</script> diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/management/index.vue index 4fdfed1b..83b52f01 100644 --- a/src/views/mall/product/management/index.vue +++ b/src/views/mall/product/management/index.vue @@ -47,12 +47,7 @@ <Icon class="mr-5px" icon="ep:refresh" /> 重置 </el-button> - <el-button - v-hasPermi="['product:brand:create']" - plain - type="primary" - @click="openForm('create')" - > + <el-button v-hasPermi="['product:brand:create']" plain type="primary" @click="openForm"> <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> @@ -133,8 +128,8 @@ </template> <script lang="ts" name="ProductManagement" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' - +import { dateFormatter } from '@/utils/formatTime' // 业务api +import * as managementApi from '@/api/mall/product/management/spu' // const message = useMessage() // 消息弹窗 // const message = useMessage() // 消息弹窗 // const { t } = useI18n() // 国际化 const { push } = useRouter() // 路由跳转 @@ -182,9 +177,9 @@ const queryFormRef = ref() // 搜索的表单 const getList = async () => { loading.value = true try { - // const data = await ProductBrandApi.getBrandParam(queryParams) - // list.value = data.list - // total.value = data.total + const data = await managementApi.getSkuList(queryParams) + list.value = data.list + total.value = data.total } finally { loading.value = false } @@ -201,7 +196,10 @@ const resetQuery = () => { handleQuery() } -const openForm = () => { +const openForm = (id?: number) => { + if (typeof id === 'number') { + push('/product/productManagementAdd?id=' + id) + } push('/product/productManagementAdd') } diff --git a/src/views/mall/product/property/index.vue b/src/views/mall/product/property/index.vue index 102ee8a5..399633bd 100644 --- a/src/views/mall/product/property/index.vue +++ b/src/views/mall/product/property/index.vue @@ -2,42 +2,49 @@ <!-- 搜索工作栏 --> <ContentWrap> <el-form - class="-mb-15px" - :model="queryParams" ref="queryFormRef" :inline="true" + :model="queryParams" + class="-mb-15px" label-width="68px" > <el-form-item label="名称" prop="name"> <el-input v-model="queryParams.name" - placeholder="请输入名称" - clearable - @keyup.enter="handleQuery" class="!w-240px" + clearable + placeholder="请输入名称" + @keyup.enter="handleQuery" /> </el-form-item> <el-form-item label="创建时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" + end-placeholder="结束日期" + start-placeholder="开始日期" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" /> </el-form-item> <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> <el-button + v-hasPermi="['product:property:create']" plain type="primary" @click="openForm('create')" - v-hasPermi="['product:property:create']" > - <Icon icon="ep:plus" class="mr-5px" /> 新增 + <Icon class="mr-5px" icon="ep:plus" /> + 新增 </el-button> </el-form-item> </el-form> @@ -46,23 +53,23 @@ <!-- 列表 --> <ContentWrap> <el-table v-loading="loading" :data="list"> - <el-table-column label="编号" align="center" prop="id" /> - <el-table-column label="名称" align="center" /> - <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" /> + <el-table-column align="center" label="编号" prop="id" /> + <el-table-column align="center" label="名称" prop="name" /> + <el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" /> <el-table-column - label="创建时间" + :formatter="dateFormatter" align="center" + label="创建时间" prop="createTime" width="180" - :formatter="dateFormatter" /> - <el-table-column label="操作" align="center"> + <el-table-column align="center" label="操作"> <template #default="scope"> <el-button + v-hasPermi="['product:property:update']" link type="primary" @click="openForm('update', scope.row.id)" - v-hasPermi="['product:property:update']" > 编辑 </el-button> @@ -70,10 +77,10 @@ <router-link :to="'/property/value/' + scope.row.id">属性值</router-link> </el-button> <el-button + v-hasPermi="['product:property:delete']" link type="danger" @click="handleDelete(scope.row.id)" - v-hasPermi="['product:property:delete']" > 删除 </el-button> @@ -82,9 +89,9 @@ </el-table> <!-- 分页 --> <Pagination - :total="total" - v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" @pagination="getList" /> </ContentWrap> @@ -92,10 +99,11 @@ <!-- 表单弹窗:添加/修改 --> <PropertyForm ref="formRef" @success="getList" /> </template> -<script setup lang="ts" name="ProductProperty"> +<script lang="ts" name="ProductProperty" setup> import { dateFormatter } from '@/utils/formatTime' import * as PropertyApi from '@/api/mall/product/property' import PropertyForm from './PropertyForm.vue' + const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 From 7a64eb51988973cb972f49b687ddcdd9b57f1bac Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sat, 29 Apr 2023 23:10:18 +0800 Subject: [PATCH 13/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=B7=BB=E5=8A=A0=E5=B1=9E=E6=80=A7=E5=92=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B1=9E=E6=80=A7=E5=80=BC=E6=97=B6=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E6=98=BE=E9=9A=90=E6=8E=A7=E5=88=B6=E3=80=81?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E7=84=A6=E7=82=B9=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E9=94=99=E4=B9=B1=E7=AD=89bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/components/BasicInfoForm.vue | 9 ++--- .../components/ProductAttributes.vue | 37 +++++++++++-------- .../components/ProductAttributesAddForm.vue | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index cd4d5c90..90f7c0df 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -98,7 +98,6 @@ import ProductAttributes from './ProductAttributes.vue' import { copyValueToTarget } from '@/utils/object' // 业务Api import * as ProductCategoryApi from '@/api/mall/product/category' -import * as PropertyApi from '@/api/mall/product/property' import { defaultProps, handleTree } from '@/utils/tree' import { ElInput } from 'element-plus' @@ -116,12 +115,11 @@ const attributeList = ref([ { id: 1, name: '颜色', - attributeValues: [{ id: 1, name: '白色' }] + values: [{ id: 1, name: '白色' }] } ]) -const addAttribute = async (propertyId: number) => { - const data = await PropertyApi.getPropertyValuePage({ id: propertyId }) - console.log(data) +const addAttribute = (property: any) => { + attributeList.value.push(property) } const formData = reactive<SpuType>({ name: '', // 商品名称 @@ -132,7 +130,6 @@ const formData = reactive<SpuType>({ sliderPicUrls: [], // 商品轮播图 introduction: '', // 商品简介 deliveryTemplateId: 1, // 运费模版 - selectRule: '', // 选择规则 TODO 暂定 specType: false, // 商品规格 subCommissionType: false, // 分销类型 skus: [ diff --git a/src/views/mall/product/management/components/ProductAttributes.vue b/src/views/mall/product/management/components/ProductAttributes.vue index 95cf67b7..120ffd36 100644 --- a/src/views/mall/product/management/components/ProductAttributes.vue +++ b/src/views/mall/product/management/components/ProductAttributes.vue @@ -7,7 +7,7 @@ <div> <el-text class="mx-1">属性值:</el-text> <el-tag - v-for="(value, valueIndex) in item.attributeValues" + v-for="(value, valueIndex) in item.values" :key="value.name" :disable-transitions="false" class="mx-1" @@ -17,7 +17,7 @@ {{ value.name }} </el-tag> <el-input - v-if="inputVisible" + v-show="inputVisible(index)" ref="InputRef" v-model="inputValue" class="!w-20" @@ -25,7 +25,12 @@ @blur="handleInputConfirm(index)" @keyup.enter="handleInputConfirm(index)" /> - <el-button v-else class="button-new-tag ml-1" size="small" @click="showInput(index)"> + <el-button + v-show="!inputVisible(index)" + class="button-new-tag ml-1" + size="small" + @click="showInput(index)" + > + 添加 </el-button> </div> @@ -37,8 +42,13 @@ import { ElInput } from 'element-plus' const inputValue = ref('') // 输入框值 -const inputVisible = ref(false) // 输入框显隐控制 -const InputRef = ref<InstanceType<typeof ElInput>>() //标签输入框Ref +const attributeIndex = ref<number | null>(null) // 获取焦点时记录当前属性项的index +// 输入框显隐控制 +const inputVisible = computed(() => (index) => { + if (attributeIndex.value === null) return false + if (attributeIndex.value === index) return true +}) +const InputRef = ref() //标签输入框Ref const attributeList = ref([]) const props = defineProps({ attributeData: { @@ -60,23 +70,20 @@ watch( ) /** 删除标签 tagValue 标签值*/ const handleClose = (index, valueIndex) => { - const av = attributeList.value[index].attributeValues - av.splice(valueIndex, 1) + attributeList.value[index].values?.splice(valueIndex, 1) } /** 显示输入框并获取焦点 */ -const showInput = (index) => { - inputVisible.value = true - nextTick(() => { - InputRef.value[index]!.input!.focus() - }) +const showInput = async (index) => { + attributeIndex.value = index + // 因为组件在ref中所以需要用索引获取对应的Ref + InputRef.value[index]!.input!.focus() } /** 输入框失去焦点或点击回车时触发 */ const handleInputConfirm = (index) => { if (inputValue.value) { - // 因为ref再循环里,所以需要index获取对应的ref - attributeList.value[index].attributeValues.push({ name: inputValue.value }) + attributeList.value[index].values.push({ name: inputValue.value }) } - inputVisible.value = false + attributeIndex.value = null inputValue.value = '' } </script> diff --git a/src/views/mall/product/management/components/ProductAttributesAddForm.vue b/src/views/mall/product/management/components/ProductAttributesAddForm.vue index 70fd2824..f498b7dd 100644 --- a/src/views/mall/product/management/components/ProductAttributesAddForm.vue +++ b/src/views/mall/product/management/components/ProductAttributesAddForm.vue @@ -62,7 +62,7 @@ const submitForm = async () => { const propertyId = await PropertyApi.createProperty(data) emit('success', { id: propertyId, ...formData.value, values: [] }) } else { - emit(res[0]) // 因为只用一个 + emit('success', res[0]) // 因为只用一个 } message.success(t('common.createSuccess')) dialogVisible.value = false From 538d1e0b6cc997e701a6c7cc22b6550ddf21cd42 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sun, 30 Apr 2023 02:26:35 +0800 Subject: [PATCH 14/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20f?= =?UTF-8?q?ix:=E6=A0=B9=E6=8D=AE=E5=95=86=E5=93=81=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=94=9F=E6=88=90=E8=A1=A8=E6=A0=BC=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mall/product/management/type/spuType.ts | 1 - .../management/components/BasicInfoForm.vue | 39 +++--- .../components/ProductAttributes.vue | 4 +- .../components/ProductAttributesAddForm.vue | 3 + .../management/components/SkuList/index.vue | 129 ++++++++++++++++-- 5 files changed, 140 insertions(+), 36 deletions(-) diff --git a/src/api/mall/product/management/type/spuType.ts b/src/api/mall/product/management/type/spuType.ts index f51bc526..5d9b65ef 100644 --- a/src/api/mall/product/management/type/spuType.ts +++ b/src/api/mall/product/management/type/spuType.ts @@ -9,7 +9,6 @@ export interface SpuType { sliderPicUrls?: string[] // 商品轮播图 introduction?: string // 商品简介 deliveryTemplateId?: number // 运费模版 - selectRule?: string // 选择规格 TODO 暂时定义 specType?: boolean // 商品规格 subCommissionType?: boolean // 分销类型 skus?: SkuType[] // sku数组 diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index 90f7c0df..d06a787d 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -60,7 +60,7 @@ </el-col> <el-col :span="12"> <el-form-item label="商品规格" props="specType"> - <el-radio-group v-model="formData.specType" @change="changeSpecType(formData.specType)"> + <el-radio-group v-model="formData.specType"> <el-radio :label="false" class="radio">单规格</el-radio> <el-radio :label="true">多规格</el-radio> </el-radio-group> @@ -76,12 +76,17 @@ </el-col> <!-- 多规格添加--> <el-col :span="24"> - <el-form-item v-if="formData.specType" label="商品属性" prop=""> - <el-button class="mr-15px" @click="AttributesAddFormRef.open()">添加规格</el-button> + <el-form-item v-if="formData.specType" label="商品属性"> + <el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open()" + >添加规格 + </el-button> <ProductAttributes :attribute-data="attributeList" /> </el-form-item> + <el-form-item v-if="formData.specType" label="批量设置"> + <SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" /> + </el-form-item> <el-form-item> - <SkuList :sku-data="formData.skus" :subCommissionType="formData.subCommissionType" /> + <SkuList :attributeList="attributeList" :prop-form-data="formData" /> </el-form-item> </el-col> </el-row> @@ -110,14 +115,8 @@ const props = defineProps({ }) const AttributesAddFormRef = ref() // 添加商品属性表单 const ProductManagementBasicInfoRef = ref() // 表单Ref -// 属性列表 -const attributeList = ref([ - { - id: 1, - name: '颜色', - values: [{ id: 1, name: '白色' }] - } -]) +const attributeList = ref([]) // 商品属性列表 +/** 添加商品属性 */ const addAttribute = (property: any) => { attributeList.value.push(property) } @@ -176,10 +175,10 @@ const rules = reactive({ unit: [required], introduction: [required], picUrl: [required], - sliderPicUrls: [required] + sliderPicUrls: [required], // deliveryTemplateId: [required], - // specType: [required], - // subCommissionType: [required], + specType: [required], + subCommissionType: [required] }) /** * 将传进来的值赋值给formData @@ -215,10 +214,7 @@ const validate = async () => { }) } defineExpose({ validate }) -// 选择规格 -const changeSpecType = (specType) => { - console.log(specType) -} + // 分销类型 const changeSubCommissionType = () => { // 默认为零,类型切换后也要重置为零 @@ -227,10 +223,7 @@ const changeSubCommissionType = () => { item.subCommissionSecondPrice = 0 } } -// 选择属性确认 -// const confirm = () => {} -// 添加规格 -// const addRule = () => {} + const categoryList = ref() // 分类树 onMounted(async () => { // 获得分类树 diff --git a/src/views/mall/product/management/components/ProductAttributes.vue b/src/views/mall/product/management/components/ProductAttributes.vue index 120ffd36..ea9b311a 100644 --- a/src/views/mall/product/management/components/ProductAttributes.vue +++ b/src/views/mall/product/management/components/ProductAttributes.vue @@ -49,10 +49,10 @@ const inputVisible = computed(() => (index) => { if (attributeIndex.value === index) return true }) const InputRef = ref() //标签输入框Ref -const attributeList = ref([]) +const attributeList = ref([]) // 商品属性列表 const props = defineProps({ attributeData: { - type: Object, + type: Array, default: () => {} } }) diff --git a/src/views/mall/product/management/components/ProductAttributesAddForm.vue b/src/views/mall/product/management/components/ProductAttributesAddForm.vue index f498b7dd..bd715dde 100644 --- a/src/views/mall/product/management/components/ProductAttributesAddForm.vue +++ b/src/views/mall/product/management/components/ProductAttributesAddForm.vue @@ -62,6 +62,9 @@ const submitForm = async () => { const propertyId = await PropertyApi.createProperty(data) emit('success', { id: propertyId, ...formData.value, values: [] }) } else { + if (res[0].values === null) { + res[0].values = [] + } emit('success', res[0]) // 因为只用一个 } message.success(t('common.createSuccess')) diff --git a/src/views/mall/product/management/components/SkuList/index.vue b/src/views/mall/product/management/components/SkuList/index.vue index fd148126..a3ecd9dd 100644 --- a/src/views/mall/product/management/components/SkuList/index.vue +++ b/src/views/mall/product/management/components/SkuList/index.vue @@ -1,10 +1,21 @@ <template> - <el-table :data="SkuData" border class="tabNumWidth" size="small"> + <el-table :data="isBatch ? SkuData : formData.skus" border class="tabNumWidth" size="small"> <el-table-column align="center" fixed="left" label="图片" min-width="100"> <template #default="{ row }"> <UploadImg v-model="row.picUrl" height="80px" width="100%" /> </template> </el-table-column> + <template v-if="formData.specType"> + <!-- 根据商品属性动态添加 --> + <el-table-column + v-for="(item, index) in tableHeaderList" + :key="index" + :label="item.label" + :prop="item.prop" + align="center" + min-width="120" + /> + </template> <el-table-column align="center" label="商品条码" min-width="120"> <template #default="{ row }"> <el-input v-model="row.barCode" :min="0" class="w-100%" /> @@ -40,7 +51,7 @@ <el-input v-model="row.volume" :min="0" class="w-100%" type="number" /> </template> </el-table-column> - <template v-if="subCommissionType"> + <template v-if="formData.subCommissionType"> <el-table-column align="center" label="一级返佣(分)" min-width="120"> <template #default="{ row }"> <el-input v-model="row.subCommissionFirstPrice" :min="0" class="w-100%" type="number" /> @@ -52,35 +63,133 @@ </template> </el-table-column> </template> + <el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80"> + <template #default> + <el-button v-if="isBatch" link size="small" type="primary">批量添加</el-button> + <el-button v-else link size="small" type="primary">删除</el-button> + </template> + </el-table-column> </el-table> </template> <script lang="ts" name="index" setup> -import { propTypes } from '@/utils/propTypes' import { UploadImg } from '@/components/UploadFile' import { PropType } from 'vue' -import type { SkuType } from '@/api/mall/product/management/type/skuType' +import { SpuType } from '@/api/mall/product/management/type/spuType' +import { propTypes } from '@/utils/propTypes' +import { SkuType } from '@/api/mall/product/management/type/skuType' const props = defineProps({ - skuData: { - type: Array as PropType<SkuType>, + propFormData: { + type: Object as PropType<SpuType>, + default: () => {} + }, + attributeList: { + type: Array, default: () => [] }, - subCommissionType: propTypes.bool.def(false) // 分销类型 + isBatch: propTypes.bool.def(false) // 是否批量操作 }) -const SkuData = ref<SkuType[]>([]) +const formData = ref<SpuType>() // 表单数据 +// 批量添加时的零时数据 +const SkuData = ref<SkuType[]>([ + { + /** + * 商品价格,单位:分 + */ + price: 0, + /** + * 市场价,单位:分 + */ + marketPrice: 0, + /** + * 成本价,单位:分 + */ + costPrice: 0, + /** + * 商品条码 + */ + barCode: '', + /** + * 图片地址 + */ + picUrl: '', + /** + * 库存 + */ + stock: 0, + /** + * 商品重量,单位:kg 千克 + */ + weight: 0, + /** + * 商品体积,单位:m^3 平米 + */ + volume: 0 + } +]) +const tableHeaderList = ref<{ prop: string; label: string }[]>([]) /** * 将传进来的值赋值给SkuData */ watch( - () => props.skuData, + () => props.propFormData, (data) => { if (!data) return - SkuData.value = data + formData.value = data }, { deep: true, immediate: true } ) +/** 监听属性列表生成相关参数和表头 */ +watch( + () => props.attributeList, + (data) => { + // 判断代理对象是否为空 + if (JSON.stringify(data) === '[]') return + // 重置表头 + tableHeaderList.value = [] + // 重置表数据 + formData.value!.skus = [] + SkuData.value = [] + // 生成表头 + data.forEach((item, index) => { + // name加属性项index区分属性值 + tableHeaderList.value.push({ prop: `name${index}`, label: item.name }) + }) + generateTableData(data) + }, + { + deep: true, + immediate: true + } +) +/** 生成表数据 */ +const generateTableData = (data: any[]) => { + // const row = { + // price: 0, + // marketPrice: 0, + // costPrice: 0, + // barCode: '', + // picUrl: '', + // stock: 0, + // weight: 0, + // volume: 0 + // } + // 先把所有的属性值取出来 + const newDataList: any[] = [] + for (const index in data) { + newDataList.push(data[index].values) + } + console.log(newDataList) +} +// const buildRow = (list: any[]) => { +// for (const index in data) { +// for (const index1 of data[index].values) { +// row[`name${index1}`] = data[index].values[index1] +// } +// } +// } </script> From 35c3545e7cf9a6e25c5ace8cb9ae8fc9993fc15f Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sun, 30 Apr 2023 17:04:31 +0800 Subject: [PATCH 15/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=B1=9E=E6=80=A7=E5=80=BC=E5=88=B0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ProductAttributes.vue | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/views/mall/product/management/components/ProductAttributes.vue b/src/views/mall/product/management/components/ProductAttributes.vue index ea9b311a..2283f483 100644 --- a/src/views/mall/product/management/components/ProductAttributes.vue +++ b/src/views/mall/product/management/components/ProductAttributes.vue @@ -8,7 +8,7 @@ <el-text class="mx-1">属性值:</el-text> <el-tag v-for="(value, valueIndex) in item.values" - :key="value.name" + :key="value.id" :disable-transitions="false" class="mx-1" closable @@ -22,8 +22,8 @@ v-model="inputValue" class="!w-20" size="small" - @blur="handleInputConfirm(index)" - @keyup.enter="handleInputConfirm(index)" + @blur="handleInputConfirm(index, item.id)" + @keyup.enter="handleInputConfirm(index, item.id)" /> <el-button v-show="!inputVisible(index)" @@ -40,7 +40,10 @@ <script lang="ts" name="ProductAttributes" setup> import { ElInput } from 'element-plus' +import * as PropertyApi from '@/api/mall/product/property' +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 const inputValue = ref('') // 输入框值 const attributeIndex = ref<number | null>(null) // 获取焦点时记录当前属性项的index // 输入框显隐控制 @@ -79,9 +82,16 @@ const showInput = async (index) => { InputRef.value[index]!.input!.focus() } /** 输入框失去焦点或点击回车时触发 */ -const handleInputConfirm = (index) => { +const handleInputConfirm = async (index, propertyId) => { if (inputValue.value) { - attributeList.value[index].values.push({ name: inputValue.value }) + // 保存属性值 + try { + const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value }) + attributeList.value[index].values.push({ id, name: inputValue.value }) + message.success(t('common.createSuccess')) + } catch { + message.error('添加失败,请重试') // TODO 缺少国际化 + } } attributeIndex.value = null inputValue.value = '' From ab1120b0ff780b1824d53b24cbed9739d6d51425 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sun, 30 Apr 2023 17:06:30 +0800 Subject: [PATCH 16/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=95=86=E5=93=81=E5=B1=9E=E6=80=A7=E6=8E=92?= =?UTF-8?q?=E5=88=97=E7=BB=84=E5=90=88=E7=AE=97=E6=B3=95=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E8=A1=A8=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/components/SkuList/index.vue | 97 +++++++++++++------ 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/src/views/mall/product/management/components/SkuList/index.vue b/src/views/mall/product/management/components/SkuList/index.vue index a3ecd9dd..4018d740 100644 --- a/src/views/mall/product/management/components/SkuList/index.vue +++ b/src/views/mall/product/management/components/SkuList/index.vue @@ -5,16 +5,19 @@ <UploadImg v-model="row.picUrl" height="80px" width="100%" /> </template> </el-table-column> - <template v-if="formData.specType"> + <template v-if="formData.specType && !isBatch"> <!-- 根据商品属性动态添加 --> <el-table-column v-for="(item, index) in tableHeaderList" :key="index" :label="item.label" - :prop="item.prop" align="center" min-width="120" - /> + > + <template #default="{ row }"> + {{ row.properties[index].value }} + </template> + </el-table-column> </template> <el-table-column align="center" label="商品条码" min-width="120"> <template #default="{ row }"> @@ -143,17 +146,77 @@ watch( immediate: true } ) +/** 生成表数据 */ +const generateTableData = (data: any[]) => { + // 构建数据结构 + const propertiesItemList = [] + for (const item of data) { + const objList = [] + for (const v of item.values) { + const obj = { propertyId: 0, valueId: 0, value: '' } + obj.propertyId = item.id + obj.valueId = v.id + obj.value = v.name + objList.push(obj) + } + propertiesItemList.push(objList) + } + build(propertiesItemList).forEach((item) => { + const row = { + properties: [], + price: 0, + marketPrice: 0, + costPrice: 0, + barCode: '', + picUrl: '', + stock: 0, + weight: 0, + volume: 0 + } + if (Array.isArray(item)) { + row.properties = item + } else { + row.properties.push(item) + } + formData.value.skus.push(row) + }) +} +/** 构建所有排列组合 */ +const build = (list: any[]) => { + if (list.length === 0) { + return [] + } else if (list.length === 1) { + return list[0] + } else { + const result = [] + const rest = build(list.slice(1)) + for (let i = 0; i < list[0].length; i++) { + for (let j = 0; j < rest.length; j++) { + // 第一次不是数组结构,后面的都是数组结构 + if (Array.isArray(rest[j])) { + result.push([list[0][i], ...rest[j]]) + } else { + result.push([list[0][i], rest[j]]) + } + } + } + return result + } +} /** 监听属性列表生成相关参数和表头 */ watch( () => props.attributeList, (data) => { + // 如果不是多规格则结束 + if (!formData.value.specType) return + // 如果当前组件作为批量添加数据使用则结束 + if (props.isBatch) return // 判断代理对象是否为空 if (JSON.stringify(data) === '[]') return // 重置表头 tableHeaderList.value = [] // 重置表数据 formData.value!.skus = [] - SkuData.value = [] // 生成表头 data.forEach((item, index) => { // name加属性项index区分属性值 @@ -166,30 +229,4 @@ watch( immediate: true } ) -/** 生成表数据 */ -const generateTableData = (data: any[]) => { - // const row = { - // price: 0, - // marketPrice: 0, - // costPrice: 0, - // barCode: '', - // picUrl: '', - // stock: 0, - // weight: 0, - // volume: 0 - // } - // 先把所有的属性值取出来 - const newDataList: any[] = [] - for (const index in data) { - newDataList.push(data[index].values) - } - console.log(newDataList) -} -// const buildRow = (list: any[]) => { -// for (const index in data) { -// for (const index1 of data[index].values) { -// row[`name${index1}`] = data[index].values[index1] -// } -// } -// } </script> From 64f6f67ddd12396d544d2c57f1e1750e22eba425 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Sun, 30 Apr 2023 17:28:16 +0800 Subject: [PATCH 17/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=95=86=E5=93=81=E5=B1=9E=E6=80=A7=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/components/SkuList/index.vue | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/views/mall/product/management/components/SkuList/index.vue b/src/views/mall/product/management/components/SkuList/index.vue index 4018d740..b46e02d0 100644 --- a/src/views/mall/product/management/components/SkuList/index.vue +++ b/src/views/mall/product/management/components/SkuList/index.vue @@ -68,7 +68,9 @@ </template> <el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80"> <template #default> - <el-button v-if="isBatch" link size="small" type="primary">批量添加</el-button> + <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd" + >批量添加 + </el-button> <el-button v-else link size="small" type="primary">删除</el-button> </template> </el-table-column> @@ -81,6 +83,7 @@ import { PropType } from 'vue' import { SpuType } from '@/api/mall/product/management/type/spuType' import { propTypes } from '@/utils/propTypes' import { SkuType } from '@/api/mall/product/management/type/skuType' +import { copyValueToTarget } from '@/utils/object' const props = defineProps({ propFormData: { @@ -131,6 +134,12 @@ const SkuData = ref<SkuType[]>([ volume: 0 } ]) +/** 批量添加 */ +const batchAdd = () => { + formData.value.skus.forEach((item) => { + copyValueToTarget(item, SkuData.value[0]) + }) +} const tableHeaderList = ref<{ prop: string; label: string }[]>([]) /** * 将传进来的值赋值给SkuData @@ -209,8 +218,21 @@ watch( (data) => { // 如果不是多规格则结束 if (!formData.value.specType) return - // 如果当前组件作为批量添加数据使用则结束 - if (props.isBatch) return + // 如果当前组件作为批量添加数据使用则重置表数据 + if (props.isBatch) { + SkuData.value = [ + { + price: 0, + marketPrice: 0, + costPrice: 0, + barCode: '', + picUrl: '', + stock: 0, + weight: 0, + volume: 0 + } + ] + } // 判断代理对象是否为空 if (JSON.stringify(data) === '[]') return // 重置表头 From 6478d295dfc0b7a01c46abf3eb0dbc1b633f4f27 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Mon, 1 May 2023 19:01:24 +0800 Subject: [PATCH 18/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mall/product/management/type/spuType.ts | 4 +- src/utils/dict.ts | 5 +- src/views/mall/product/management/addForm.vue | 97 ++++++++++++++++--- .../management/components/BasicInfoForm.vue | 97 +++++++++---------- .../{SkuList/index.vue => SkuList.vue} | 58 ++++++++--- .../product/management/components/index.ts | 12 ++- 6 files changed, 195 insertions(+), 78 deletions(-) rename src/views/mall/product/management/components/{SkuList/index.vue => SkuList.vue} (80%) diff --git a/src/api/mall/product/management/type/spuType.ts b/src/api/mall/product/management/type/spuType.ts index 5d9b65ef..f0deaed3 100644 --- a/src/api/mall/product/management/type/spuType.ts +++ b/src/api/mall/product/management/type/spuType.ts @@ -2,9 +2,9 @@ import { SkuType } from './skuType' export interface SpuType { name?: string // 商品名称 - categoryId?: number | undefined // 商品分类 + categoryId?: number | null // 商品分类 keyword?: string // 关键字 - unit?: string // 单位 + unit?: number | null // 单位 picUrl?: string // 商品封面图 sliderPicUrls?: string[] // 商品轮播图 introduction?: string // 商品简介 diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 03e17e75..f4e77c22 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -144,5 +144,8 @@ export enum DICT_TYPE { // ========== MP 模块 ========== MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型 - MP_MESSAGE_TYPE = 'mp_message_type' // 消息类型 + MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 + + // ========== MALL 模块 ========== + PRODUCT_UNIT = 'product_unit' // 商品单位 } diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index ad973394..673aa2a3 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -34,8 +34,7 @@ <script lang="ts" name="ProductManagementForm" setup> import { useTagsViewStore } from '@/store/modules/tagsView' import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' -import type { SpuType } from '@/api/mall/product/management/type/spuType' -// 业务api +import type { SpuType } from '@/api/mall/product/management/type/spuType' // 业务api import * as managementApi from '@/api/mall/product/management/spu' const { t } = useI18n() // 国际化 @@ -50,18 +49,67 @@ const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Re const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref const formData = ref<SpuType>({ - name: '', // 商品名称 - categoryId: undefined, // 商品分类 - keyword: '', // 关键字 - unit: '', // 单位 - picUrl: '', // 商品封面图 - sliderPicUrls: [], // 商品轮播图 - introduction: '', // 商品简介 + name: '213', // 商品名称 + categoryId: null, // 商品分类 + keyword: '213', // 关键字 + unit: null, // 单位 + picUrl: + 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', // 商品封面图 + sliderPicUrls: [ + { + name: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', + url: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png' + } + ], // 商品轮播图 + introduction: '213', // 商品简介 deliveryTemplateId: 0, // 运费模版 - selectRule: '', specType: false, // 商品规格 subCommissionType: false, // 分销类型 - description: '', // 商品详情 + skus: [ + { + /** + * 商品价格,单位:分 + */ + price: 0, + /** + * 市场价,单位:分 + */ + marketPrice: 0, + /** + * 成本价,单位:分 + */ + costPrice: 0, + /** + * 商品条码 + */ + barCode: '', + /** + * 图片地址 + */ + picUrl: '', + /** + * 库存 + */ + stock: 0, + /** + * 商品重量,单位:kg 千克 + */ + weight: 0, + /** + * 商品体积,单位:m^3 平米 + */ + volume: 0, + /** + * 一级分销的佣金,单位:分 + */ + subCommissionFirstPrice: 0, + /** + * 二级分销的佣金,单位:分 + */ + subCommissionSecondPrice: 0 + } + ], + description: '5425', // 商品详情 sort: 1, // 商品排序 giveIntegral: 1, // 赠送积分 virtualSalesCount: 1, // 虚拟销量 @@ -83,14 +131,38 @@ const getDetail = async () => { const submitForm = async () => { // 提交请求 formLoading.value = true + const newSkus = [...formData.value.skus] //复制一份skus保存失败时使用 // TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息 // 校验各表单 try { await unref(BasicInfoRef)?.validate() await unref(DescriptionRef)?.validate() await unref(OtherSettingsRef)?.validate() + // 处理掉一些无关数据 + formData.value.skus.forEach((item) => { + // 给sku name赋值 + item.name = formData.value.name + // 多规格情况移除skus相关属性值value + if (formData.value.specType) { + item.properties.forEach((item2) => { + delete item2.value + }) + } + }) + // 处理轮播图列表 + const newSliderPicUrls = [] + formData.value.sliderPicUrls.forEach((item) => { + // 如果是前端选的图 + if (typeof item === 'object') { + newSliderPicUrls.push(item.url) + } else { + newSliderPicUrls.push(item) + } + }) + formData.value.sliderPicUrls = newSliderPicUrls // 校验都通过后提交表单 const data = formData.value as SpuType + // 移除skus. const id = query.id as unknown as number if (!id) { await managementApi.createSpu(data) @@ -99,6 +171,9 @@ const submitForm = async () => { await managementApi.updateSpu(data) message.success(t('common.updateSuccess')) } + } catch (e) { + console.log(e) + console.log(newSkus) } finally { formLoading.value = false } diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index d06a787d..b04a9ef3 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -25,7 +25,14 @@ </el-col> <el-col :span="12"> <el-form-item label="单位" prop="unit"> - <el-input v-model="formData.unit" placeholder="请输入单位" /> + <el-select v-model="formData.unit" placeholder="请选择单位"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> </el-form-item> </el-col> <el-col :span="12"> @@ -60,7 +67,7 @@ </el-col> <el-col :span="12"> <el-form-item label="商品规格" props="specType"> - <el-radio-group v-model="formData.specType"> + <el-radio-group v-model="formData.specType" @change="onChangeSpec"> <el-radio :label="false" class="radio">单规格</el-radio> <el-radio :label="true">多规格</el-radio> </el-radio-group> @@ -82,10 +89,15 @@ </el-button> <ProductAttributes :attribute-data="attributeList" /> </el-form-item> - <el-form-item v-if="formData.specType" label="批量设置"> - <SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" /> - </el-form-item> - <el-form-item> + <template v-if="formData.specType && attributeList.length > 0"> + <el-form-item label="批量设置"> + <SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" /> + </el-form-item> + <el-form-item label="属性列表"> + <SkuList :attributeList="attributeList" :prop-form-data="formData" /> + </el-form-item> + </template> + <el-form-item v-if="!formData.specType"> <SkuList :attributeList="attributeList" :prop-form-data="formData" /> </el-form-item> </el-col> @@ -95,16 +107,15 @@ </template> <script lang="ts" name="ProductManagementBasicInfoForm" setup> import { PropType } from 'vue' -import type { SpuType } from '@/api/mall/product/management/type/spuType' -import { UploadImg, UploadImgs } from '@/components/UploadFile' -import SkuList from './SkuList/index.vue' -import ProductAttributesAddForm from './ProductAttributesAddForm.vue' -import ProductAttributes from './ProductAttributes.vue' -import { copyValueToTarget } from '@/utils/object' -// 业务Api -import * as ProductCategoryApi from '@/api/mall/product/category' import { defaultProps, handleTree } from '@/utils/tree' import { ElInput } from 'element-plus' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' +import type { SpuType } from '@/api/mall/product/management/type/spuType' +import { UploadImg, UploadImgs } from '@/components/UploadFile' +import { copyValueToTarget } from '@/utils/object' +import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index' +// 业务Api +import * as ProductCategoryApi from '@/api/mall/product/category' const message = useMessage() // 消息弹窗 const props = defineProps({ @@ -131,42 +142,7 @@ const formData = reactive<SpuType>({ deliveryTemplateId: 1, // 运费模版 specType: false, // 商品规格 subCommissionType: false, // 分销类型 - skus: [ - { - /** - * 商品价格,单位:分 - */ - price: 0, - /** - * 市场价,单位:分 - */ - marketPrice: 0, - /** - * 成本价,单位:分 - */ - costPrice: 0, - /** - * 商品条码 - */ - barCode: '', - /** - * 图片地址 - */ - picUrl: '', - /** - * 库存 - */ - stock: 0, - /** - * 商品重量,单位:kg 千克 - */ - weight: 0, - /** - * 商品体积,单位:m^3 平米 - */ - volume: 0 - } - ] + skus: [] }) const rules = reactive({ name: [required], @@ -223,6 +199,27 @@ const changeSubCommissionType = () => { item.subCommissionSecondPrice = 0 } } +// 选择规格 +const onChangeSpec = () => { + console.log(111) + // 重置商品属性列表 + attributeList.value = [] + // 重置sku列表 + formData.skus = [ + { + price: 0, + marketPrice: 0, + costPrice: 0, + barCode: '', + picUrl: '', + stock: 0, + weight: 0, + volume: 0, + subCommissionFirstPrice: 0, + subCommissionSecondPrice: 0 + } + ] +} const categoryList = ref() // 分类树 onMounted(async () => { diff --git a/src/views/mall/product/management/components/SkuList/index.vue b/src/views/mall/product/management/components/SkuList.vue similarity index 80% rename from src/views/mall/product/management/components/SkuList/index.vue rename to src/views/mall/product/management/components/SkuList.vue index b46e02d0..9ed41f50 100644 --- a/src/views/mall/product/management/components/SkuList/index.vue +++ b/src/views/mall/product/management/components/SkuList.vue @@ -21,48 +21,68 @@ </template> <el-table-column align="center" label="商品条码" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.barCode" :min="0" class="w-100%" /> + <el-input v-model="row.barCode" class="w-100%" /> </template> </el-table-column> <el-table-column align="center" label="销售价(分)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.price" :min="0" class="w-100%" type="number" /> + <el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> <el-table-column align="center" label="市场价(分)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.marketPrice" :min="0" class="w-100%" type="number" /> + <el-input-number + v-model="row.marketPrice" + :min="0" + class="w-100%" + controls-position="right" + /> </template> </el-table-column> <el-table-column align="center" label="成本价(分)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.costPrice" :min="0" class="w-100%" type="number" /> + <el-input-number + v-model="row.costPrice" + :min="0" + class="w-100%" + controls-position="right" + /> </template> </el-table-column> <el-table-column align="center" label="库存" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.stock" :min="0" class="w-100%" type="number" /> + <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> <el-table-column align="center" label="重量(kg)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.weight" :min="0" class="w-100%" type="number" /> + <el-input-number v-model="row.weight" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> <el-table-column align="center" label="体积(m^3)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.volume" :min="0" class="w-100%" type="number" /> + <el-input-number v-model="row.volume" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> <template v-if="formData.subCommissionType"> <el-table-column align="center" label="一级返佣(分)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.subCommissionFirstPrice" :min="0" class="w-100%" type="number" /> + <el-input-number + v-model="row.subCommissionFirstPrice" + :min="0" + class="w-100%" + controls-position="right" + /> </template> </el-table-column> <el-table-column align="center" label="二级返佣(分)" min-width="120"> <template #default="{ row }"> - <el-input v-model="row.subCommissionSecondPrice" :min="0" class="w-100%" type="number" /> + <el-input-number + v-model="row.subCommissionSecondPrice" + :min="0" + class="w-100%" + controls-position="right" + /> </template> </el-table-column> </template> @@ -77,7 +97,7 @@ </el-table> </template> -<script lang="ts" name="index" setup> +<script lang="ts" name="SkuList" setup> import { UploadImg } from '@/components/UploadFile' import { PropType } from 'vue' import { SpuType } from '@/api/mall/product/management/type/spuType' @@ -131,7 +151,15 @@ const SkuData = ref<SkuType[]>([ /** * 商品体积,单位:m^3 平米 */ - volume: 0 + volume: 0, + /** + * 一级分销的佣金,单位:分 + */ + subCommissionFirstPrice: 0, + /** + * 二级分销的佣金,单位:分 + */ + subCommissionSecondPrice: 0 } ]) /** 批量添加 */ @@ -180,7 +208,9 @@ const generateTableData = (data: any[]) => { picUrl: '', stock: 0, weight: 0, - volume: 0 + volume: 0, + subCommissionFirstPrice: 0, + subCommissionSecondPrice: 0 } if (Array.isArray(item)) { row.properties = item @@ -229,7 +259,9 @@ watch( picUrl: '', stock: 0, weight: 0, - volume: 0 + volume: 0, + subCommissionFirstPrice: 0, + subCommissionSecondPrice: 0 } ] } diff --git a/src/views/mall/product/management/components/index.ts b/src/views/mall/product/management/components/index.ts index 04e6f74d..f908e7d3 100644 --- a/src/views/mall/product/management/components/index.ts +++ b/src/views/mall/product/management/components/index.ts @@ -1,5 +1,15 @@ import BasicInfoForm from './BasicInfoForm.vue' import DescriptionForm from './DescriptionForm.vue' import OtherSettingsForm from './OtherSettingsForm.vue' +import ProductAttributes from './ProductAttributes.vue' +import ProductAttributesAddForm from './ProductAttributesAddForm.vue' +import SkuList from './SkuList.vue' -export { BasicInfoForm, DescriptionForm, OtherSettingsForm } +export { + BasicInfoForm, + DescriptionForm, + OtherSettingsForm, + ProductAttributes, + ProductAttributesAddForm, + SkuList +} From 1116fb278bc5799839d7102322c0b5558374e70e Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Tue, 2 May 2023 02:20:58 +0800 Subject: [PATCH 19/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/spu.ts | 6 +-- src/utils/dict.ts | 3 +- .../management/components/BasicInfoForm.vue | 4 +- .../management/components/DescriptionForm.vue | 4 +- .../components/OtherSettingsForm.vue | 4 +- .../product/management/components/SkuList.vue | 18 ++++----- src/views/mall/product/management/index.vue | 40 +++++++++---------- 7 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/api/mall/product/management/spu.ts b/src/api/mall/product/management/spu.ts index d5bf52ee..1205b0f2 100644 --- a/src/api/mall/product/management/spu.ts +++ b/src/api/mall/product/management/spu.ts @@ -1,9 +1,9 @@ import request from '@/config/axios' import type { SpuType } from './type/spuType' -// 获得sku列表 -export const getSkuList = (params: any) => { - return request.get({ url: '/product/sku/list', params }) +// 获得spu列表 +export const getSpuList = (params: any) => { + return request.get({ url: '/product/spu/page', params }) } // 创建商品spu export const createSpu = (data: SpuType) => { diff --git a/src/utils/dict.ts b/src/utils/dict.ts index f4e77c22..d11debc9 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -147,5 +147,6 @@ export enum DICT_TYPE { MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型 // ========== MALL 模块 ========== - PRODUCT_UNIT = 'product_unit' // 商品单位 + PRODUCT_UNIT = 'product_unit', // 商品单位 + PRODUCT_SPU_STATUS = 'product_spu_status' //商品状态 } diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index b04a9ef3..1d3f93df 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -116,13 +116,15 @@ import { copyValueToTarget } from '@/utils/object' import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index' // 业务Api import * as ProductCategoryApi from '@/api/mall/product/category' +import { propTypes } from '@/utils/propTypes' const message = useMessage() // 消息弹窗 const props = defineProps({ propFormData: { type: Object as PropType<SpuType>, default: () => {} - } + }, + activeName: propTypes.string.def('') }) const AttributesAddFormRef = ref() // 添加商品属性表单 const ProductManagementBasicInfoRef = ref() // 表单Ref diff --git a/src/views/mall/product/management/components/DescriptionForm.vue b/src/views/mall/product/management/components/DescriptionForm.vue index 541ff6b5..17f6e00f 100644 --- a/src/views/mall/product/management/components/DescriptionForm.vue +++ b/src/views/mall/product/management/components/DescriptionForm.vue @@ -11,13 +11,15 @@ import type { SpuType } from '@/api/mall/product/management/type/spuType' import { Editor } from '@/components/Editor' import { PropType } from 'vue' import { copyValueToTarget } from '@/utils/object' +import { propTypes } from '@/utils/propTypes' const message = useMessage() // 消息弹窗 const props = defineProps({ propFormData: { type: Object as PropType<SpuType>, default: () => {} - } + }, + activeName: propTypes.string.def('') }) const DescriptionFormRef = ref() // 表单Ref const formData = ref<SpuType>({ diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue index 106eb748..4469962d 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -53,13 +53,15 @@ import type { SpuType } from '@/api/mall/product/management/type/spuType' import { PropType } from 'vue' import { copyValueToTarget } from '@/utils/object' +import { propTypes } from '@/utils/propTypes' const message = useMessage() // 消息弹窗 const props = defineProps({ propFormData: { type: Object as PropType<SpuType>, default: () => {} - } + }, + activeName: propTypes.string.def('') }) // 商品推荐选项 const recommend = [ diff --git a/src/views/mall/product/management/components/SkuList.vue b/src/views/mall/product/management/components/SkuList.vue index 9ed41f50..7ac596e9 100644 --- a/src/views/mall/product/management/components/SkuList.vue +++ b/src/views/mall/product/management/components/SkuList.vue @@ -19,17 +19,17 @@ </template> </el-table-column> </template> - <el-table-column align="center" label="商品条码" min-width="120"> + <el-table-column align="center" label="商品条码" min-width="168"> <template #default="{ row }"> <el-input v-model="row.barCode" class="w-100%" /> </template> </el-table-column> - <el-table-column align="center" label="销售价(分)" min-width="120"> + <el-table-column align="center" label="销售价(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> - <el-table-column align="center" label="市场价(分)" min-width="120"> + <el-table-column align="center" label="市场价(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.marketPrice" @@ -39,7 +39,7 @@ /> </template> </el-table-column> - <el-table-column align="center" label="成本价(分)" min-width="120"> + <el-table-column align="center" label="成本价(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.costPrice" @@ -49,23 +49,23 @@ /> </template> </el-table-column> - <el-table-column align="center" label="库存" min-width="120"> + <el-table-column align="center" label="库存" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> - <el-table-column align="center" label="重量(kg)" min-width="120"> + <el-table-column align="center" label="重量(kg)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.weight" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> - <el-table-column align="center" label="体积(m^3)" min-width="120"> + <el-table-column align="center" label="体积(m^3)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.volume" :min="0" class="w-100%" controls-position="right" /> </template> </el-table-column> <template v-if="formData.subCommissionType"> - <el-table-column align="center" label="一级返佣(分)" min-width="120"> + <el-table-column align="center" label="一级返佣(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.subCommissionFirstPrice" @@ -75,7 +75,7 @@ /> </template> </el-table-column> - <el-table-column align="center" label="二级返佣(分)" min-width="120"> + <el-table-column align="center" label="二级返佣(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.subCommissionSecondPrice" diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/management/index.vue index 83b52f01..5b2ff9eb 100644 --- a/src/views/mall/product/management/index.vue +++ b/src/views/mall/product/management/index.vue @@ -68,7 +68,7 @@ <el-table v-loading="loading" :data="list"> <el-table-column type="expand"> <template #default="{ row }"> - <el-form class="demo-table-expand" inline label-position="left"> + <el-form inline label-position="left"> <el-form-item label="市场价:"> <span>{{ row.marketPrice }}</span> </el-form-item> @@ -83,23 +83,18 @@ </el-table-column> <el-table-column label="商品图" min-width="80"> <template #default="{ row }"> - <div class="demo-image__preview"> + <div class="demo-image__preview z-100"> <el-image - :preview-src-list="[row.image]" - :src="row.image" + :src="row.picUrl" style="width: 36px; height: 36px" + @click="imgViewVisible = true" /> </div> </template> </el-table-column> - <el-table-column - :show-overflow-tooltip="true" - label="商品名称" - min-width="300" - prop="storeName" - /> + <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" /> <el-table-column align="center" label="商品售价" min-width="90" prop="price" /> - <el-table-column align="center" label="销量" min-width="90" prop="sales" /> + <el-table-column align="center" label="销量" min-width="90" prop="salesCount" /> <el-table-column align="center" label="库存" min-width="90" prop="stock" /> <el-table-column align="center" label="排序" min-width="70" prop="sort" /> <el-table-column @@ -112,7 +107,7 @@ <el-table-column fixed="right" label="状态" min-width="80"> <template #default="{ row }"> <!--TODO 暂时用COMMON_STATUS占位一下使其不报错 --> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" /> + <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="row.status" /> </template> </el-table-column> <el-table-column align="center" fixed="right" label="操作" min-width="150" /> @@ -125,11 +120,19 @@ @pagination="getList" /> </ContentWrap> + <!-- 必须在表格外面展示。不然单元格会遮挡图层 --> + <el-image-viewer + v-if="imgViewVisible" + :url-list="[ + 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png' + ]" + @close="imgViewVisible = false" + /> </template> <script lang="ts" name="ProductManagement" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' // 业务api -import * as managementApi from '@/api/mall/product/management/spu' // const message = useMessage() // 消息弹窗 +import * as managementApi from '@/api/mall/product/management/spu' // const message = useMessage() // 消息弹窗 // const { t } = useI18n() // 国际化 const { push } = useRouter() // 路由跳转 @@ -163,13 +166,10 @@ const headerNum = ref([ type: 5 } ]) +const imgViewVisible = ref(false) const queryParams = reactive({ pageNo: 1, - pageSize: 10, - name: undefined, - status: undefined, - createTime: [], - type: '1' + pageSize: 10 }) const queryFormRef = ref() // 搜索的表单 @@ -177,7 +177,7 @@ const queryFormRef = ref() // 搜索的表单 const getList = async () => { loading.value = true try { - const data = await managementApi.getSkuList(queryParams) + const data = await managementApi.getSpuList(queryParams) list.value = data.list total.value = data.total } finally { @@ -218,6 +218,6 @@ const openForm = (id?: number) => { /** 初始化 **/ onMounted(() => { - // getList() + getList() }) </script> From 9ee35fc165a546a09b85f5667cc2a8b8e728993f Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Wed, 3 May 2023 02:28:35 +0800 Subject: [PATCH 20/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91,=E5=AE=8C=E6=88=90=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E4=BF=9D=E5=AD=98=E5=92=8C=E6=95=B0=E6=8D=AE=E5=9B=9E?= =?UTF-8?q?=E6=98=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/sku.ts | 0 src/api/mall/product/management/spu.ts | 4 + .../mall/product/management/type/skuType.ts | 4 + .../mall/product/management/type/spuType.ts | 3 +- src/api/mall/product/property.ts | 4 +- src/views/mall/product/management/addForm.vue | 19 ++++- .../management/components/BasicInfoForm.vue | 7 +- .../product/management/components/SkuList.vue | 32 ++++++-- src/views/mall/product/management/index.vue | 76 +++++++++++++------ 9 files changed, 112 insertions(+), 37 deletions(-) delete mode 100644 src/api/mall/product/management/sku.ts diff --git a/src/api/mall/product/management/sku.ts b/src/api/mall/product/management/sku.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/api/mall/product/management/spu.ts b/src/api/mall/product/management/spu.ts index 1205b0f2..007963bb 100644 --- a/src/api/mall/product/management/spu.ts +++ b/src/api/mall/product/management/spu.ts @@ -13,3 +13,7 @@ export const createSpu = (data: SpuType) => { export const updateSpu = (data: SpuType) => { return request.put({ url: '/product/spu/update', data }) } +// 获得商品spu +export const getSpu = (id: number) => { + return request.get({ url: `/product/spu/get-detail?id=${id}` }) +} diff --git a/src/api/mall/product/management/type/skuType.ts b/src/api/mall/product/management/type/skuType.ts index 6de0d893..42889dc4 100644 --- a/src/api/mall/product/management/type/skuType.ts +++ b/src/api/mall/product/management/type/skuType.ts @@ -11,6 +11,10 @@ export interface Property { * 关联 {@link ProductPropertyValueDO#getId()} */ valueId?: number + /** + * 属性值名称 + */ + valueName?: string } export interface SkuType { diff --git a/src/api/mall/product/management/type/spuType.ts b/src/api/mall/product/management/type/spuType.ts index f0deaed3..11c3c888 100644 --- a/src/api/mall/product/management/type/spuType.ts +++ b/src/api/mall/product/management/type/spuType.ts @@ -1,6 +1,7 @@ import { SkuType } from './skuType' export interface SpuType { + id?: number name?: string // 商品名称 categoryId?: number | null // 商品分类 keyword?: string // 关键字 @@ -11,7 +12,7 @@ export interface SpuType { deliveryTemplateId?: number // 运费模版 specType?: boolean // 商品规格 subCommissionType?: boolean // 分销类型 - skus?: SkuType[] // sku数组 + skus: SkuType[] // sku数组 description?: string // 商品详情 sort?: string // 商品排序 giveIntegral?: number // 赠送积分 diff --git a/src/api/mall/product/property.ts b/src/api/mall/product/property.ts index 01c79f9f..ac8bac59 100644 --- a/src/api/mall/product/property.ts +++ b/src/api/mall/product/property.ts @@ -71,8 +71,8 @@ export const getPropertyList = (params: any) => { } // 获得属性项列表 -export const getPropertyListAndValue = (params: any) => { - return request.get({ url: '/product/property/get-value-list', params }) +export const getPropertyListAndValue = (data: any) => { + return request.post({ url: '/product/property/get-value-list', data }) } // ------------------------ 属性值 ------------------- diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index 673aa2a3..27f8a8ae 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -36,6 +36,7 @@ import { useTagsViewStore } from '@/store/modules/tagsView' import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' import type { SpuType } from '@/api/mall/product/management/type/spuType' // 业务api import * as managementApi from '@/api/mall/product/management/spu' +import * as PropertyApi from '@/api/mall/product/property' const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 @@ -122,8 +123,20 @@ const formData = ref<SpuType>({ /** 获得详情 */ const getDetail = async () => { const id = query.id as unknown as number - if (!id) { - return + if (id) { + formLoading.value = true + try { + const res = (await managementApi.getSpu(id)) as SpuType + formData.value = res + // 直接取第一个值就能得到所有属性的id + const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId) + const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds }) + await nextTick() + // 回显商品属性 + BasicInfoRef.value.addAttribute(PropertyS) + } finally { + formLoading.value = false + } } } @@ -145,7 +158,7 @@ const submitForm = async () => { // 多规格情况移除skus相关属性值value if (formData.value.specType) { item.properties.forEach((item2) => { - delete item2.value + delete item2.valueName }) } }) diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/management/components/BasicInfoForm.vue index 1d3f93df..1b60c17e 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/management/components/BasicInfoForm.vue @@ -131,6 +131,10 @@ const ProductManagementBasicInfoRef = ref() // 表单Ref const attributeList = ref([]) // 商品属性列表 /** 添加商品属性 */ const addAttribute = (property: any) => { + if (Array.isArray(property)) { + attributeList.value = property + return + } attributeList.value.push(property) } const formData = reactive<SpuType>({ @@ -191,7 +195,7 @@ const validate = async () => { } }) } -defineExpose({ validate }) +defineExpose({ validate, addAttribute }) // 分销类型 const changeSubCommissionType = () => { @@ -203,7 +207,6 @@ const changeSubCommissionType = () => { } // 选择规格 const onChangeSpec = () => { - console.log(111) // 重置商品属性列表 attributeList.value = [] // 重置sku列表 diff --git a/src/views/mall/product/management/components/SkuList.vue b/src/views/mall/product/management/components/SkuList.vue index 7ac596e9..2a7441cc 100644 --- a/src/views/mall/product/management/components/SkuList.vue +++ b/src/views/mall/product/management/components/SkuList.vue @@ -1,5 +1,11 @@ <template> - <el-table :data="isBatch ? SkuData : formData.skus" border class="tabNumWidth" size="small"> + <el-table + :data="isBatch ? SkuData : formData.skus" + border + class="tabNumWidth" + max-height="500" + size="small" + > <el-table-column align="center" fixed="left" label="图片" min-width="100"> <template #default="{ row }"> <UploadImg v-model="row.picUrl" height="80px" width="100%" /> @@ -15,7 +21,7 @@ min-width="120" > <template #default="{ row }"> - {{ row.properties[index].value }} + {{ row.properties[index]?.valueName }} </template> </el-table-column> </template> @@ -190,15 +196,28 @@ const generateTableData = (data: any[]) => { for (const item of data) { const objList = [] for (const v of item.values) { - const obj = { propertyId: 0, valueId: 0, value: '' } + const obj = { propertyId: 0, valueId: 0, valueName: '' } obj.propertyId = item.id obj.valueId = v.id - obj.value = v.name + obj.valueName = v.name objList.push(obj) } propertiesItemList.push(objList) } - build(propertiesItemList).forEach((item) => { + const buildList = build(propertiesItemList) + // 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理 (解决编辑表单时或查看详情时数据回显问题) + console.log( + buildList.length === formData.value.skus.length || data.some((item) => item.values.length === 0) + ) + if ( + buildList.length === formData.value.skus.length || + data.some((item) => item.values.length === 0) + ) { + return + } + // 重置表数据 + formData.value!.skus = [] + buildList.forEach((item) => { const row = { properties: [], price: 0, @@ -212,6 +231,7 @@ const generateTableData = (data: any[]) => { subCommissionFirstPrice: 0, subCommissionSecondPrice: 0 } + // 判断是否是单一属性的情况 if (Array.isArray(item)) { row.properties = item } else { @@ -269,8 +289,6 @@ watch( if (JSON.stringify(data) === '[]') return // 重置表头 tableHeaderList.value = [] - // 重置表数据 - formData.value!.skus = [] // 生成表头 data.forEach((item, index) => { // name加属性项index区分属性值 diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/management/index.vue index 5b2ff9eb..f2e480f5 100644 --- a/src/views/mall/product/management/index.vue +++ b/src/views/mall/product/management/index.vue @@ -87,7 +87,7 @@ <el-image :src="row.picUrl" style="width: 36px; height: 36px" - @click="imgViewVisible = true" + @click="imagePreview(row.picUrl)" /> </div> </template> @@ -106,11 +106,38 @@ /> <el-table-column fixed="right" label="状态" min-width="80"> <template #default="{ row }"> - <!--TODO 暂时用COMMON_STATUS占位一下使其不报错 --> - <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="row.status" /> + <el-switch + v-model="row.status" + :active-value="0" + :disabled="Number(row.status) < 0" + :inactive-value="1" + active-text="上架" + inactive-text="下架" + inline-prompt + @change="changeStatus(row)" + /> + </template> + </el-table-column> + <el-table-column align="center" fixed="right" label="操作" min-width="150"> + <template #default="{ row }"> + <el-button + v-hasPermi="['product:spu:query']" + link + type="primary" + @click="openForm(row.id)" + > + 修改 + </el-button> + <el-button + v-hasPermi="['product:spu:update']" + link + type="primary" + @click="changeStatus(row)" + > + 加入回收站 + </el-button> </template> </el-table-column> - <el-table-column align="center" fixed="right" label="操作" min-width="150" /> </el-table> <!-- 分页 --> <Pagination @@ -123,9 +150,7 @@ <!-- 必须在表格外面展示。不然单元格会遮挡图层 --> <el-image-viewer v-if="imgViewVisible" - :url-list="[ - 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png' - ]" + :url-list="imageViewerList" @close="imgViewVisible = false" /> </template> @@ -166,7 +191,8 @@ const headerNum = ref([ type: 5 } ]) -const imgViewVisible = ref(false) +const imgViewVisible = ref(false) // 商品图预览 +const imageViewerList = ref<string[]>([]) // 商品图预览列表 const queryParams = reactive({ pageNo: 1, pageSize: 10 @@ -184,7 +210,21 @@ const getList = async () => { loading.value = false } } - +/** + * 更改SPU状态 + * @param row + */ +const changeStatus = (row) => { + console.log(row) +} +/** + * 商品图预览 + * @param imgUrl + */ +const imagePreview = (imgUrl: string) => { + imageViewerList.value = [imgUrl] + imgViewVisible.value = true +} /** 搜索按钮操作 */ const handleQuery = () => { getList() @@ -196,26 +236,18 @@ const resetQuery = () => { handleQuery() } +/** + * 新增或修改 + * @param id + */ const openForm = (id?: number) => { if (typeof id === 'number') { push('/product/productManagementAdd?id=' + id) + return } push('/product/productManagementAdd') } -/** 删除按钮操作 */ -// const handleDelete = async (id: number) => { -// try { -// // 删除的二次确认 -// await message.delConfirm() -// // 发起删除 -// await ProductBrandApi.deleteBrand(id) -// message.success(t('common.delSuccess')) -// // 刷新列表 -// await getList() -// } catch {} -// } - /** 初始化 **/ onMounted(() => { getList() From 0c6e3a39c95190bb54538d548c4df4f0ff4d57b4 Mon Sep 17 00:00:00 2001 From: puhui999 <puhui999@163.com> Date: Thu, 4 May 2023 01:38:53 +0800 Subject: [PATCH 21/28] =?UTF-8?q?=E5=95=86=E5=93=81=E7=AE=A1=E7=90=86:=20?= =?UTF-8?q?=E6=89=93=E9=80=9A=E6=89=80=E6=9C=89=E6=8E=A5=E5=8F=A3=EF=BC=88?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/spu.ts | 12 + src/utils/constants.ts | 17 ++ src/views/mall/product/management/addForm.vue | 9 +- .../components/OtherSettingsForm.vue | 1 + .../product/management/components/SkuList.vue | 3 - src/views/mall/product/management/index.vue | 234 +++++++++++++----- 6 files changed, 206 insertions(+), 70 deletions(-) diff --git a/src/api/mall/product/management/spu.ts b/src/api/mall/product/management/spu.ts index 007963bb..2aa83e7a 100644 --- a/src/api/mall/product/management/spu.ts +++ b/src/api/mall/product/management/spu.ts @@ -5,6 +5,10 @@ import type { SpuType } from './type/spuType' export const getSpuList = (params: any) => { return request.get({ url: '/product/spu/page', params }) } +// 获得spu列表tabsCount +export const getTabsCount = () => { + return request.get({ url: '/product/spu/tabsCount' }) +} // 创建商品spu export const createSpu = (data: SpuType) => { return request.post({ url: '/product/spu/create', data }) @@ -13,7 +17,15 @@ export const createSpu = (data: SpuType) => { export const updateSpu = (data: SpuType) => { return request.put({ url: '/product/spu/update', data }) } +// 更新商品spu status +export const updateStatus = (data: { id: number; status: number }) => { + return request.put({ url: '/product/spu/updateStatus', data }) +} // 获得商品spu export const getSpu = (id: number) => { return request.get({ url: `/product/spu/get-detail?id=${id}` }) } +// 删除商品Spu +export const deleteSpu = (id: number) => { + return request.delete({ url: `/product/spu/delete?id=${id}` }) +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5cda391f..597064ee 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -220,3 +220,20 @@ export const PayRefundStatusEnum = { name: '退款关闭' } } +/** + * 商品SPU枚举类 + */ +export const ProductSpuStatusEnum = { + RECYCLE: { + status: -1, + name: '回收站' + }, + DISABLE: { + status: 0, + name: '下架' + }, + ENABLE: { + status: 1, + name: '上架' + } +} diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/management/addForm.vue index 27f8a8ae..7f31871f 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/management/addForm.vue @@ -144,7 +144,7 @@ const getDetail = async () => { const submitForm = async () => { // 提交请求 formLoading.value = true - const newSkus = [...formData.value.skus] //复制一份skus保存失败时使用 + const newSkus = JSON.parse(JSON.stringify(formData.value.skus)) //深拷贝一份skus保存失败时使用 // TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息 // 校验各表单 try { @@ -184,9 +184,12 @@ const submitForm = async () => { await managementApi.updateSpu(data) message.success(t('common.updateSuccess')) } + close() } catch (e) { - console.log(e) - console.log(newSkus) + // 如果是后端校验失败,恢复skus数据 + if (typeof e === 'string') { + formData.value.skus = newSkus + } } finally { formLoading.value = false } diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/management/components/OtherSettingsForm.vue index 4469962d..bd0ca26d 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/management/components/OtherSettingsForm.vue @@ -117,6 +117,7 @@ watch( (data) => { if (!data) return copyValueToTarget(formData.value, data) + // TODO 如果先修改其他设置的值,再改变商品详情或是商品信息会重置其他设置页面中的相关值 下一个版本修复 checkboxGroup.value = [] formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : '' formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : '' diff --git a/src/views/mall/product/management/components/SkuList.vue b/src/views/mall/product/management/components/SkuList.vue index 2a7441cc..3311202a 100644 --- a/src/views/mall/product/management/components/SkuList.vue +++ b/src/views/mall/product/management/components/SkuList.vue @@ -206,9 +206,6 @@ const generateTableData = (data: any[]) => { } const buildList = build(propertiesItemList) // 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理 (解决编辑表单时或查看详情时数据回显问题) - console.log( - buildList.length === formData.value.skus.length || data.some((item) => item.values.length === 0) - ) if ( buildList.length === formData.value.skus.length || data.some((item) => item.values.length === 0) diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/management/index.vue index f2e480f5..7b5fb0c1 100644 --- a/src/views/mall/product/management/index.vue +++ b/src/views/mall/product/management/index.vue @@ -57,39 +57,38 @@ <!-- 列表 --> <ContentWrap> - <el-tabs v-model="queryParams.type" @tab-click="getList"> + <el-tabs v-model="queryParams.tabType" @tab-click="handleClick"> <el-tab-pane - v-for="(item, index) in headerNum" - :key="index" + v-for="item in tabsData" + :key="item.type" :label="item.name + '(' + item.count + ')'" - :name="item.type.toString()" + :name="item.type" /> </el-tabs> <el-table v-loading="loading" :data="list"> - <el-table-column type="expand"> - <template #default="{ row }"> - <el-form inline label-position="left"> - <el-form-item label="市场价:"> - <span>{{ row.marketPrice }}</span> - </el-form-item> - <el-form-item label="成本价:"> - <span>{{ row.costPrice }}</span> - </el-form-item> - <el-form-item label="虚拟销量:"> - <span>{{ row.virtualSalesCount }}</span> - </el-form-item> - </el-form> - </template> - </el-table-column> + <!-- TODO 暂时不做折叠数据 --> + <!-- <el-table-column type="expand">--> + <!-- <template #default="{ row }">--> + <!-- <el-form inline label-position="left">--> + <!-- <el-form-item label="市场价:">--> + <!-- <span>{{ row.marketPrice }}</span>--> + <!-- </el-form-item>--> + <!-- <el-form-item label="成本价:">--> + <!-- <span>{{ row.costPrice }}</span>--> + <!-- </el-form-item>--> + <!-- <el-form-item label="虚拟销量:">--> + <!-- <span>{{ row.virtualSalesCount }}</span>--> + <!-- </el-form-item>--> + <!-- </el-form>--> + <!-- </template>--> + <!-- </el-table-column>--> <el-table-column label="商品图" min-width="80"> <template #default="{ row }"> - <div class="demo-image__preview z-100"> - <el-image - :src="row.picUrl" - style="width: 36px; height: 36px" - @click="imagePreview(row.picUrl)" - /> - </div> + <el-image + :src="row.picUrl" + style="width: 36px; height: 36px" + @click="imagePreview(row.picUrl)" + /> </template> </el-table-column> <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" /> @@ -108,9 +107,9 @@ <template #default="{ row }"> <el-switch v-model="row.status" - :active-value="0" + :active-value="1" :disabled="Number(row.status) < 0" - :inactive-value="1" + :inactive-value="0" active-text="上架" inactive-text="下架" inline-prompt @@ -120,22 +119,42 @@ </el-table-column> <el-table-column align="center" fixed="right" label="操作" min-width="150"> <template #default="{ row }"> - <el-button - v-hasPermi="['product:spu:query']" - link - type="primary" - @click="openForm(row.id)" - > - 修改 - </el-button> - <el-button - v-hasPermi="['product:spu:update']" - link - type="primary" - @click="changeStatus(row)" - > - 加入回收站 - </el-button> + <template v-if="queryParams.tabType === 4"> + <el-button + v-hasPermi="['product:spu:delete']" + link + type="danger" + @click="handleDelete(row.id)" + > + 删除 + </el-button> + <el-button + v-hasPermi="['product:spu:update']" + link + type="primary" + @click="addToTrash(row, ProductSpuStatusEnum.DISABLE.status)" + > + 恢复到仓库 + </el-button> + </template> + <template v-else> + <el-button + v-hasPermi="['product:spu:update']" + link + type="primary" + @click="openForm(row.id)" + > + 修改 + </el-button> + <el-button + v-hasPermi="['product:spu:update']" + link + type="primary" + @click="addToTrash(row, ProductSpuStatusEnum.RECYCLE.status)" + > + 加入回收站 + </el-button> + </template> </template> </el-table-column> </el-table> @@ -158,64 +177,141 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { dateFormatter } from '@/utils/formatTime' // 业务api import * as managementApi from '@/api/mall/product/management/spu' -// const message = useMessage() // 消息弹窗 -// const { t } = useI18n() // 国际化 -const { push } = useRouter() // 路由跳转 +import { ProductSpuStatusEnum } from '@/utils/constants' +import { TabsPaneContext } from 'element-plus' + +const message = useMessage() // 消息弹窗 +const { t } = useI18n() // 国际化 +const { currentRoute, push } = useRouter() // 路由跳转 const loading = ref(false) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref<any[]>([]) // 列表的数据 -const headerNum = ref([ +// tabs数据 +const tabsData = ref([ { - count: 8, + count: 0, name: '出售中商品', - type: 1 + type: 0 }, { count: 0, name: '仓库中商品', + type: 1 + }, + { + count: 0, + name: '已经售空商品', type: 2 }, { count: 0, - name: '已经售馨商品', + name: '警戒库存', type: 3 }, - { - count: 0, - name: '警戒库存', - type: 4 - }, { count: 0, name: '商品回收站', - type: 5 + type: 4 } ]) +const getTabsCount = async () => { + try { + const res = await managementApi.getTabsCount() + for (let objName in res) { + tabsData.value[Number(objName)].count = res[objName] + } + } catch {} +} const imgViewVisible = ref(false) // 商品图预览 const imageViewerList = ref<string[]>([]) // 商品图预览列表 -const queryParams = reactive({ +const queryParams = ref({ pageNo: 1, - pageSize: 10 + pageSize: 10, + tabType: 0 }) const queryFormRef = ref() // 搜索的表单 - +const handleClick = (tab: TabsPaneContext) => { + queryParams.value.tabType = tab.paneName + getList() +} /** 查询列表 */ const getList = async () => { loading.value = true try { - const data = await managementApi.getSpuList(queryParams) + const data = await managementApi.getSpuList(queryParams.value) list.value = data.list total.value = data.total } finally { loading.value = false } } + /** * 更改SPU状态 * @param row + * @param status 更改前的值 */ -const changeStatus = (row) => { - console.log(row) +const changeStatus = async (row, status?: number) => { + // TODO 测试过程中似乎有点问题,下一版修复 + try { + let text = '' + switch (row.status) { + case ProductSpuStatusEnum.DISABLE.status: + text = ProductSpuStatusEnum.DISABLE.name + break + case ProductSpuStatusEnum.ENABLE.status: + text = ProductSpuStatusEnum.ENABLE.name + break + case ProductSpuStatusEnum.RECYCLE.status: + text = `加入${ProductSpuStatusEnum.RECYCLE.name}` + break + } + await message.confirm( + row.status === -1 ? `确认要将[${row.name}]${text}吗?` : `确认要${text}[${row.name}]吗?` + ) + await managementApi.updateStatus({ id: row.id, status: row.status }) + message.success('更新状态成功') + // 刷新tabs数据 + await getTabsCount() + // 刷新列表 + await getList() + } catch { + // 取消加入回收站时回显数据 + if (typeof status !== 'undefined') { + row.status = status + return + } + // 取消更改状态时回显数据 + row.status = + row.status === ProductSpuStatusEnum.DISABLE.status + ? ProductSpuStatusEnum.ENABLE.status + : ProductSpuStatusEnum.DISABLE.status + } +} +/** + * 加入回收站 + * @param row + * @param status + */ +const addToTrash = (row, status) => { + // 复制一份原值 + const num = Number(`${row.status}`) + row.status = status + changeStatus(row, num) +} +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await managementApi.deleteSpu(id) + message.success(t('common.delSuccess')) + // 刷新tabs数据 + await getTabsCount() + // 刷新列表 + await getList() + } catch {} } /** * 商品图预览 @@ -247,9 +343,19 @@ const openForm = (id?: number) => { } push('/product/productManagementAdd') } - +// 监听路由变化更新列表 +watch( + () => currentRoute.value, + () => { + getList() + }, + { + immediate: true + } +) /** 初始化 **/ onMounted(() => { + getTabsCount() getList() }) </script> From 1c77ba8e0450d939f70e1bec2c7fc0183c701412 Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Thu, 4 May 2023 21:00:49 +0800 Subject: [PATCH 22/28] =?UTF-8?q?code=20review=20=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/mp/components/wx-msg/components/MsgEvent.vue | 2 -- src/views/mp/components/wx-msg/components/MsgList.vue | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/views/mp/components/wx-msg/components/MsgEvent.vue b/src/views/mp/components/wx-msg/components/MsgEvent.vue index e13e3112..d23c9366 100644 --- a/src/views/mp/components/wx-msg/components/MsgEvent.vue +++ b/src/views/mp/components/wx-msg/components/MsgEvent.vue @@ -47,5 +47,3 @@ const props = defineProps<{ const item = ref(props.item) </script> - -<style scoped></style> diff --git a/src/views/mp/components/wx-msg/components/MsgList.vue b/src/views/mp/components/wx-msg/components/MsgList.vue index 39f7203c..561d619a 100644 --- a/src/views/mp/components/wx-msg/components/MsgList.vue +++ b/src/views/mp/components/wx-msg/components/MsgList.vue @@ -18,7 +18,7 @@ class="avue-comment__body" :style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''" > - <!-- 【事件】区域 --> + <!-- 【事件】区域 TODO 芋艿:是不是把拆个 Message 出来,里面包括 MsgEvent + 各种其它消息,分开有点不够整体 --> <MsgEvent v-if="item.type === MsgType.Event" :item="item" /> <!-- 【消息】区域 --> <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> @@ -68,7 +68,6 @@ </div> </div> </template> - <script setup lang="ts" name="MsgList"> import WxVideoPlayer from '@/views/mp/components/wx-video-play' import WxVoicePlayer from '@/views/mp/components/wx-voice-play' From 2b84489969663c9308f01c1156d914e86c068998 Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Thu, 4 May 2023 22:45:12 +0800 Subject: [PATCH 23/28] =?UTF-8?q?refactor:=20mp/wx-msg=E6=8B=86=E5=88=86Ms?= =?UTF-8?q?g=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mp/components/wx-msg/components/Msg.vue | 67 +++++++++++++++++++ .../components/wx-msg/components/MsgList.vue | 55 +-------------- src/views/mp/components/wx-msg/main.vue | 2 +- 3 files changed, 71 insertions(+), 53 deletions(-) create mode 100644 src/views/mp/components/wx-msg/components/Msg.vue diff --git a/src/views/mp/components/wx-msg/components/Msg.vue b/src/views/mp/components/wx-msg/components/Msg.vue new file mode 100644 index 00000000..eff834c9 --- /dev/null +++ b/src/views/mp/components/wx-msg/components/Msg.vue @@ -0,0 +1,67 @@ +<template> + <div> + <MsgEvent v-if="item.type === MsgType.Event" :item="item" /> + + <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> + + <div v-else-if="item.type === MsgType.Voice"> + <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> + </div> + + <div v-else-if="item.type === MsgType.Image"> + <a target="_blank" :href="item.mediaUrl"> + <img :src="item.mediaUrl" style="width: 100px" /> + </a> + </div> + + <div + v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" + style="text-align: center" + > + <WxVideoPlayer :url="item.mediaUrl" /> + </div> + + <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> + <el-link type="success" :underline="false" target="_blank" :href="item.url"> + <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> + </el-link> + <div class="avue-card__info" style="height: unset">{{ item.description }}</div> + </div> + + <div v-else-if="item.type === MsgType.Location"> + <WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" /> + </div> + + <div v-else-if="item.type === MsgType.News" style="width: 300px"> + <WxNews :articles="item.articles" /> + </div> + + <div v-else-if="item.type === MsgType.Music"> + <WxMusic + :title="item.title" + :description="item.description" + :thumb-media-url="item.thumbMediaUrl" + :music-url="item.musicUrl" + :hq-music-url="item.hqMusicUrl" + /> + </div> + </div> +</template> + +<script setup lang="ts" name="Msg"> +import WxVideoPlayer from '@/views/mp/components/wx-video-play' +import WxVoicePlayer from '@/views/mp/components/wx-voice-play' +import WxNews from '@/views/mp/components/wx-news' +import WxLocation from '@/views/mp/components/wx-location' +import WxMusic from '@/views/mp/components/wx-music' +import MsgEvent from './MsgEvent.vue' +import { MsgType } from '../types' + +const props = defineProps<{ + item: any +}>() + +const item = ref<any>(props.item) +</script> + +<style scoped></style> diff --git a/src/views/mp/components/wx-msg/components/MsgList.vue b/src/views/mp/components/wx-msg/components/MsgList.vue index 561d619a..f759adda 100644 --- a/src/views/mp/components/wx-msg/components/MsgList.vue +++ b/src/views/mp/components/wx-msg/components/MsgList.vue @@ -18,65 +18,16 @@ class="avue-comment__body" :style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''" > - <!-- 【事件】区域 TODO 芋艿:是不是把拆个 Message 出来,里面包括 MsgEvent + 各种其它消息,分开有点不够整体 --> - <MsgEvent v-if="item.type === MsgType.Event" :item="item" /> - <!-- 【消息】区域 --> - <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div> - <div v-else-if="item.type === MsgType.Voice"> - <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" /> - </div> - <div v-else-if="item.type === MsgType.Image"> - <a target="_blank" :href="item.mediaUrl"> - <img :src="item.mediaUrl" style="width: 100px" /> - </a> - </div> - <div - v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'" - style="text-align: center" - > - <WxVideoPlayer :url="item.mediaUrl" /> - </div> - <div v-else-if="item.type === MsgType.Link" class="avue-card__detail"> - <el-link type="success" :underline="false" target="_blank" :href="item.url"> - <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div> - </el-link> - <div class="avue-card__info" style="height: unset">{{ item.description }}</div> - </div> - <!-- TODO 芋艿:待完善 --> - <div v-else-if="item.type === MsgType.Location"> - <WxLocation - :label="item.label" - :location-y="item.locationY" - :location-x="item.locationX" - /> - </div> - <div v-else-if="item.type === MsgType.News" style="width: 300px"> - <!-- TODO 芋艿:待测试;详情页也存在类似的情况 --> - <WxNews :articles="item.articles" /> - </div> - <div v-else-if="item.type === MsgType.Music"> - <WxMusic - :title="item.title" - :description="item.description" - :thumb-media-url="item.thumbMediaUrl" - :music-url="item.musicUrl" - :hq-music-url="item.hqMusicUrl" - /> - </div> + <Msg :item="item" /> </div> </div> </div> </div> </template> <script setup lang="ts" name="MsgList"> -import WxVideoPlayer from '@/views/mp/components/wx-video-play' -import WxVoicePlayer from '@/views/mp/components/wx-voice-play' -import WxNews from '@/views/mp/components/wx-news' -import WxLocation from '@/views/mp/components/wx-location' -import WxMusic from '@/views/mp/components/wx-music' -import MsgEvent from './MsgEvent.vue' +import Msg from './Msg.vue' import { formatDate } from '@/utils/formatTime' -import { MsgType, User } from '../types' +import { User } from '../types' import avatarWechat from '@/assets/imgs/wechat.png' const props = defineProps<{ diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue index 079e9740..1eeab64a 100644 --- a/src/views/mp/components/wx-msg/main.vue +++ b/src/views/mp/components/wx-msg/main.vue @@ -74,7 +74,7 @@ const reply = ref<Reply>({ }) const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelect组件ref,用于消息发送成功后清除内容 -const msgDivRef = ref() // 消息显示窗口ref,用于滚动到底部 +const msgDivRef = ref<HTMLDivElement | null>(null) // 消息显示窗口ref,用于滚动到底部 /** 完成加载 */ onMounted(async () => { From be49c381f6ffebe618c782be9b66931f7e85690a Mon Sep 17 00:00:00 2001 From: dhb52 <dhb52@126.com> Date: Fri, 5 May 2023 23:04:47 +0800 Subject: [PATCH 24/28] =?UTF-8?q?refactor:=20mp/autoReply=20=E6=8B=86?= =?UTF-8?q?=E5=88=86ReplyForm=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mp/autoReply/components/ReplyForm.vue | 78 +++++++++++++++++++ src/views/mp/autoReply/index.vue | 72 +++++------------ 2 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 src/views/mp/autoReply/components/ReplyForm.vue diff --git a/src/views/mp/autoReply/components/ReplyForm.vue b/src/views/mp/autoReply/components/ReplyForm.vue new file mode 100644 index 00000000..edcbc696 --- /dev/null +++ b/src/views/mp/autoReply/components/ReplyForm.vue @@ -0,0 +1,78 @@ +<template> + <div> + <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px"> + <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message"> + <el-select v-model="replyForm.requestMessageType" placeholder="请选择"> + <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> + <el-option + v-if="RequestMessageTypes.includes(dict.value)" + :label="dict.label" + :value="dict.value" + /> + </template> + </el-select> + </el-form-item> + <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword"> + <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword"> + <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable /> + </el-form-item> + <el-form-item label="回复消息"> + <WxReplySelect v-model="reply" /> + </el-form-item> + </el-form> + </div> +</template> + +<script setup lang="ts" name="ReplyForm"> +import WxReplySelect, { type Reply } from '@/views/mp/components/wx-reply' +import type { FormInstance } from 'element-plus' +import { MsgType } from './types' +import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict' + +const props = defineProps<{ + modelValue: any + reply: Reply + msgType: MsgType +}>() + +const emit = defineEmits<{ + (e: 'update:reply', v: Reply) + (e: 'update:modelValue', v: any) +}>() + +const reply = computed<Reply>({ + get: () => props.reply, + set: (val) => emit('update:reply', val) +}) + +const replyForm = computed<any>({ + get: () => props.modelValue, + set: (val) => emit('update:modelValue', val) +}) + +const formRef = ref<FormInstance | null>(null) // 表单 ref + +const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型 + +// 表单校验 +const rules = { + requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }], + requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }] +} + +defineExpose({ + resetFields: () => formRef.value?.resetFields(), + validate: async () => formRef.value?.validate() +}) +</script> + +<style scoped></style> diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue index e2fcd7a0..b3826de5 100644 --- a/src/views/mp/autoReply/index.vue +++ b/src/views/mp/autoReply/index.vue @@ -53,38 +53,13 @@ @on-delete="onDelete" /> - <!-- 添加或修改自动回复的对话框 --> - <!-- TODO @Dhb52 --> - <el-dialog :title="dialogTitle" v-model="showFormDialog" width="800px" destroy-on-close> - <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px"> - <el-form-item label="消息类型" prop="requestMessageType" v-if="msgType === MsgType.Message"> - <el-select v-model="replyForm.requestMessageType" placeholder="请选择"> - <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value"> - <el-option - v-if="RequestMessageTypes.includes(dict.value)" - :label="dict.label" - :value="dict.value" - /> - </template> - </el-select> - </el-form-item> - <el-form-item label="匹配类型" prop="requestMatch" v-if="msgType === MsgType.Keyword"> - <el-select v-model="replyForm.requestMatch" placeholder="请选择匹配类型" clearable> - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="关键词" prop="requestKeyword" v-if="msgType === MsgType.Keyword"> - <el-input v-model="replyForm.requestKeyword" placeholder="请输入内容" clearable /> - </el-form-item> - <el-form-item label="回复消息"> - <WxReplySelect v-model="reply" /> - </el-form-item> - </el-form> + <el-dialog + :title="isCreating ? '新增自动回复' : '修改自动回复'" + v-model="showDialog" + width="800px" + destroy-on-close + > + <ReplyForm v-model="replyForm" v-model:reply="reply" :msg-type="msgType" ref="formRef" /> <template #footer> <el-button @click="cancel">取 消</el-button> <el-button type="primary" @click="onSubmit">确 定</el-button> @@ -93,23 +68,22 @@ </ContentWrap> </template> <script setup lang="ts" name="MpAutoReply"> -import WxReplySelect, { type Reply, ReplyType } from '@/views/mp/components/wx-reply' +import ReplyForm from '@/views/mp/autoReply/components/ReplyForm.vue' +import { type Reply, ReplyType } from '@/views/mp/components/wx-reply' import WxAccountSelect from '@/views/mp/components/wx-account-select' import * as MpAutoReplyApi from '@/api/mp/autoReply' -import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict' import { ContentWrap } from '@/components/ContentWrap' -import type { FormInstance, TabPaneName } from 'element-plus' +import type { TabPaneName } from 'element-plus' import ReplyTable from './components/ReplyTable.vue' import { MsgType } from './components/types' const message = useMessage() // 消息 const accountId = ref(-1) // 公众号ID const msgType = ref<MsgType>(MsgType.Keyword) // 消息类型 -const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 允许选择的请求消息类型 const loading = ref(true) // 遮罩层 const total = ref(0) // 总条数 const list = ref<any[]>([]) // 自动回复列表 -const formRef = ref<FormInstance | null>(null) // 表单 ref +const formRef = ref<InstanceType<typeof ReplyForm> | null>(null) // 表单 ref // 查询参数 const queryParams = reactive({ pageNo: 1, @@ -117,19 +91,14 @@ const queryParams = reactive({ accountId: accountId }) -const dialogTitle = ref('') // 弹出层标题 -const showFormDialog = ref(false) // 是否显示弹出层 +const isCreating = ref(false) // 是否新建(否则编辑) +const showDialog = ref(false) // 是否显示弹出层 const replyForm = ref<any>({}) // 表单参数 // 回复消息 const reply = ref<Reply>({ type: ReplyType.Text, accountId: -1 }) -// 表单校验 -const rules = { - requestKeyword: [{ required: true, message: '请求的关键字不能为空', trigger: 'blur' }], - requestMatch: [{ required: true, message: '请求的关键字的匹配不能为空', trigger: 'blur' }] -} /** 侦听账号变化 */ const onAccountChanged = (id: number) => { @@ -174,8 +143,8 @@ const onCreate = () => { accountId: queryParams.accountId } - dialogTitle.value = '新增自动回复' - showFormDialog.value = true + isCreating.value = true + showDialog.value = true } /** 修改按钮操作 */ @@ -207,8 +176,8 @@ const onUpdate = async (id: number) => { } // 打开表单 - dialogTitle.value = '修改自动回复' - showFormDialog.value = true + isCreating.value = false + showDialog.value = true } /** 删除按钮操作 */ @@ -220,8 +189,7 @@ const onDelete = async (id: number) => { } const onSubmit = async () => { - const valid = await formRef.value?.validate() - if (!valid) return + await formRef.value?.validate() // 处理回复消息 const submitForm: any = { ...replyForm.value } @@ -245,7 +213,7 @@ const onSubmit = async () => { message.success('新增成功') } - showFormDialog.value = false + showDialog.value = false await getList() } @@ -264,7 +232,7 @@ const reset = () => { // 取消按钮 const cancel = () => { - showFormDialog.value = false + showDialog.value = false reset() } </script> From e92361ed401771998c6abd6d9ba53801a6e0cfa6 Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Sat, 6 May 2023 22:54:41 +0800 Subject: [PATCH 25/28] =?UTF-8?q?code=20review=20=E5=95=86=E5=93=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mall/product/management/spu.ts | 16 +++++-- src/router/modules/remaining.ts | 4 +- src/utils/constants.ts | 1 + src/utils/object.ts | 1 + .../product/{management => spu}/addForm.vue | 8 +++- .../components/BasicInfoForm.vue | 33 +++++++------- .../components/DescriptionForm.vue | 13 +++--- .../components/OtherSettingsForm.vue | 18 ++++---- .../components/ProductAttributes.vue | 3 ++ .../components/ProductAttributesAddForm.vue | 0 .../components/SkuList.vue | 16 +++++-- .../{management => spu}/components/index.ts | 0 .../product/{management => spu}/index.vue | 43 +++++++++++++++---- 13 files changed, 108 insertions(+), 48 deletions(-) rename src/views/mall/product/{management => spu}/addForm.vue (94%) rename src/views/mall/product/{management => spu}/components/BasicInfoForm.vue (90%) rename src/views/mall/product/{management => spu}/components/DescriptionForm.vue (99%) rename src/views/mall/product/{management => spu}/components/OtherSettingsForm.vue (93%) rename src/views/mall/product/{management => spu}/components/ProductAttributes.vue (99%) rename src/views/mall/product/{management => spu}/components/ProductAttributesAddForm.vue (100%) rename src/views/mall/product/{management => spu}/components/SkuList.vue (95%) rename src/views/mall/product/{management => spu}/components/index.ts (100%) rename src/views/mall/product/{management => spu}/index.vue (89%) diff --git a/src/api/mall/product/management/spu.ts b/src/api/mall/product/management/spu.ts index 2aa83e7a..07d7103e 100644 --- a/src/api/mall/product/management/spu.ts +++ b/src/api/mall/product/management/spu.ts @@ -1,30 +1,38 @@ import request from '@/config/axios' -import type { SpuType } from './type/spuType' +import type { SpuType } from './type/spuType' // TODO @puhui999: type 和 api 一起放,简单一点哈~ -// 获得spu列表 -export const getSpuList = (params: any) => { +// TODO @puhui999:中英文之间有空格 + +// 获得spu列表 TODO @puhui999:这个是 getSpuPage 哈 +export const getSpuList = (params: PageParam) => { return request.get({ url: '/product/spu/page', params }) } + // 获得spu列表tabsCount export const getTabsCount = () => { return request.get({ url: '/product/spu/tabsCount' }) } + // 创建商品spu export const createSpu = (data: SpuType) => { return request.post({ url: '/product/spu/create', data }) } + // 更新商品spu export const updateSpu = (data: SpuType) => { return request.put({ url: '/product/spu/update', data }) } + // 更新商品spu status export const updateStatus = (data: { id: number; status: number }) => { return request.put({ url: '/product/spu/updateStatus', data }) } -// 获得商品spu + +// 获得商品 spu export const getSpu = (id: number) => { return request.get({ url: `/product/spu/get-detail?id=${id}` }) } + // 删除商品Spu export const deleteSpu = (id: number) => { return request.delete({ url: `/product/spu/delete?id=${id}` }) diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 32848b9a..4f5a16bd 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -355,8 +355,8 @@ const remainingRouter: AppRouteRecordRaw[] = [ }, children: [ { - path: 'productManagementAdd', - component: () => import('@/views/mall/product/management/addForm.vue'), + path: 'productManagementAdd', // TODO @puhui999:最好拆成 add 和 edit 两个路由;添加商品;修改商品 + component: () => import('@/views/mall/product/spu/addForm.vue'), name: 'ProductManagementAdd', meta: { noCache: true, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 597064ee..e37b6abc 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -220,6 +220,7 @@ export const PayRefundStatusEnum = { name: '退款关闭' } } + /** * 商品SPU枚举类 */ diff --git a/src/utils/object.ts b/src/utils/object.ts index 8edd1888..6612da74 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -1,3 +1,4 @@ +// TODO @puhui999:这个方法,可以考虑放到 index.js /** * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2} * @param target 目标对象 diff --git a/src/views/mall/product/management/addForm.vue b/src/views/mall/product/spu/addForm.vue similarity index 94% rename from src/views/mall/product/management/addForm.vue rename to src/views/mall/product/spu/addForm.vue index 7f31871f..28fc414d 100644 --- a/src/views/mall/product/management/addForm.vue +++ b/src/views/mall/product/spu/addForm.vue @@ -37,7 +37,6 @@ import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' import type { SpuType } from '@/api/mall/product/management/type/spuType' // 业务api import * as managementApi from '@/api/mall/product/management/spu' import * as PropertyApi from '@/api/mall/product/property' - const { t } = useI18n() // 国际化 const message = useMessage() // 消息弹窗 const { push, currentRoute } = useRouter() // 路由 @@ -69,7 +68,7 @@ const formData = ref<SpuType>({ skus: [ { /** - * 商品价格,单位:分 + * 商品价格,单位:分 TODO @puhui999:注释放在尾巴哈,简洁一点~ */ price: 0, /** @@ -120,6 +119,7 @@ const formData = ref<SpuType>({ recommendNew: false, // 是否新品 recommendGood: false // 是否优品 }) + /** 获得详情 */ const getDetail = async () => { const id = query.id as unknown as number @@ -129,6 +129,7 @@ const getDetail = async () => { const res = (await managementApi.getSpu(id)) as SpuType formData.value = res // 直接取第一个值就能得到所有属性的id + // TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法 const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId) const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds }) await nextTick() @@ -151,6 +152,7 @@ const submitForm = async () => { await unref(BasicInfoRef)?.validate() await unref(DescriptionRef)?.validate() await unref(OtherSettingsRef)?.validate() + // TODO @puhui:直接做深拷贝?这样最终 server 端不满足,不需要恢复 // 处理掉一些无关数据 formData.value.skus.forEach((item) => { // 给sku name赋值 @@ -166,6 +168,7 @@ const submitForm = async () => { const newSliderPicUrls = [] formData.value.sliderPicUrls.forEach((item) => { // 如果是前端选的图 + // TODO @puhui999:疑问哈,为啥会是 object 呀? if (typeof item === 'object') { newSliderPicUrls.push(item.url) } else { @@ -224,6 +227,7 @@ const resetForm = async () => { } /** 关闭按钮 */ const close = () => { + // TODO @puhui999:是不是不用 reset 呀?close 默认销毁 resetForm() delView(unref(currentRoute)) push('/product/product-management') diff --git a/src/views/mall/product/management/components/BasicInfoForm.vue b/src/views/mall/product/spu/components/BasicInfoForm.vue similarity index 90% rename from src/views/mall/product/management/components/BasicInfoForm.vue rename to src/views/mall/product/spu/components/BasicInfoForm.vue index 1b60c17e..249a3830 100644 --- a/src/views/mall/product/management/components/BasicInfoForm.vue +++ b/src/views/mall/product/spu/components/BasicInfoForm.vue @@ -7,6 +7,7 @@ </el-form-item> </el-col> <el-col :span="12"> + <!-- TODO @puhui999:只能选根节点 --> <el-form-item label="商品分类" prop="categoryId"> <el-tree-select v-model="formData.categoryId" @@ -15,6 +16,7 @@ check-strictly node-key="id" placeholder="请选择商品分类" + class="w-1/1" /> </el-form-item> </el-col> @@ -25,7 +27,7 @@ </el-col> <el-col :span="12"> <el-form-item label="单位" prop="unit"> - <el-select v-model="formData.unit" placeholder="请选择单位"> + <el-select v-model="formData.unit" placeholder="请选择单位" class="w-1/1"> <el-option v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)" :key="dict.value" @@ -57,7 +59,7 @@ </el-col> <el-col :span="12"> <el-form-item label="运费模板" prop="deliveryTemplateId"> - <el-select v-model="formData.deliveryTemplateId" placeholder="请选择" style="width: 100%"> + <el-select v-model="formData.deliveryTemplateId" placeholder="请选择" class="w-1/1"> <el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> @@ -84,9 +86,8 @@ <!-- 多规格添加--> <el-col :span="24"> <el-form-item v-if="formData.specType" label="商品属性"> - <el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open()" - >添加规格 - </el-button> + <!-- TODO @puhui999:参考 https://admin.java.crmeb.net/store/list/creatProduct 添加规格好做么?添加的时候,不用输入备注哈 --> + <el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open">添加规格</el-button> <ProductAttributes :attribute-data="attributeList" /> </el-form-item> <template v-if="formData.specType && attributeList.length > 0"> @@ -108,17 +109,15 @@ <script lang="ts" name="ProductManagementBasicInfoForm" setup> import { PropType } from 'vue' import { defaultProps, handleTree } from '@/utils/tree' -import { ElInput } from 'element-plus' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import type { SpuType } from '@/api/mall/product/management/type/spuType' import { UploadImg, UploadImgs } from '@/components/UploadFile' import { copyValueToTarget } from '@/utils/object' import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index' -// 业务Api import * as ProductCategoryApi from '@/api/mall/product/category' import { propTypes } from '@/utils/propTypes' - const message = useMessage() // 消息弹窗 + const props = defineProps({ propFormData: { type: Object as PropType<SpuType>, @@ -126,10 +125,11 @@ const props = defineProps({ }, activeName: propTypes.string.def('') }) -const AttributesAddFormRef = ref() // 添加商品属性表单 -const ProductManagementBasicInfoRef = ref() // 表单Ref +const AttributesAddFormRef = ref() // 添加商品属性表单 TODO @puhui999:小写开头哈 +const ProductManagementBasicInfoRef = ref() // 表单Ref TODO @puhui999:小写开头哈 +// TODO @puhui999:attributeList 改成 propertyList,会更统一一点 const attributeList = ref([]) // 商品属性列表 -/** 添加商品属性 */ +/** 添加商品属性 */ // TODO @puhui999:propFormData 算出来 const addAttribute = (property: any) => { if (Array.isArray(property)) { attributeList.value = property @@ -162,8 +162,9 @@ const rules = reactive({ specType: [required], subCommissionType: [required] }) + /** - * 将传进来的值赋值给formData + * 将传进来的值赋值给 formData */ watch( () => props.propFormData, @@ -176,10 +177,11 @@ watch( immediate: true } ) -const emit = defineEmits(['update:activeName']) + /** * 表单校验 */ +const emit = defineEmits(['update:activeName']) const validate = async () => { // 校验表单 if (!ProductManagementBasicInfoRef) return @@ -197,7 +199,7 @@ const validate = async () => { } defineExpose({ validate, addAttribute }) -// 分销类型 +/** 分销类型 */ const changeSubCommissionType = () => { // 默认为零,类型切换后也要重置为零 for (const item of formData.skus) { @@ -205,7 +207,8 @@ const changeSubCommissionType = () => { item.subCommissionSecondPrice = 0 } } -// 选择规格 + +/** 选择规格 */ const onChangeSpec = () => { // 重置商品属性列表 attributeList.value = [] diff --git a/src/views/mall/product/management/components/DescriptionForm.vue b/src/views/mall/product/spu/components/DescriptionForm.vue similarity index 99% rename from src/views/mall/product/management/components/DescriptionForm.vue rename to src/views/mall/product/spu/components/DescriptionForm.vue index 17f6e00f..0a7f522b 100644 --- a/src/views/mall/product/management/components/DescriptionForm.vue +++ b/src/views/mall/product/spu/components/DescriptionForm.vue @@ -25,6 +25,11 @@ const DescriptionFormRef = ref() // 表单Ref const formData = ref<SpuType>({ description: '' // 商品详情 }) +// 表单规则 +const rules = reactive({ + description: [required] +}) + /** * 富文本编辑器如果输入过再清空会有残留,需再重置一次 */ @@ -40,10 +45,7 @@ watch( immediate: true } ) -// 表单规则 -const rules = reactive({ - description: [required] -}) + /** * 将传进来的值赋值给formData */ @@ -58,10 +60,11 @@ watch( immediate: true } ) -const emit = defineEmits(['update:activeName']) + /** * 表单校验 */ +const emit = defineEmits(['update:activeName']) const validate = async () => { // 校验表单 if (!DescriptionFormRef) return diff --git a/src/views/mall/product/management/components/OtherSettingsForm.vue b/src/views/mall/product/spu/components/OtherSettingsForm.vue similarity index 93% rename from src/views/mall/product/management/components/OtherSettingsForm.vue rename to src/views/mall/product/spu/components/OtherSettingsForm.vue index bd0ca26d..c0fc5122 100644 --- a/src/views/mall/product/management/components/OtherSettingsForm.vue +++ b/src/views/mall/product/spu/components/OtherSettingsForm.vue @@ -1,6 +1,7 @@ <template> <el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px"> <el-row> + <!-- TODO @puhui999:横着三个哈 --> <el-col :span="24"> <el-col :span="8"> <el-form-item label="商品排序" prop="sort"> @@ -40,6 +41,7 @@ <el-tag class="ml-2" type="warning">拼团</el-tag> </el-form-item> </el-col> + <!-- TODO @puhui999:等优惠劵 ok 在搞 --> <el-col :span="24"> <el-form-item label="赠送优惠劵"> <el-button>选择优惠券</el-button> @@ -49,13 +51,12 @@ </el-form> </template> <script lang="ts" name="OtherSettingsForm" setup> -// 商品推荐 import type { SpuType } from '@/api/mall/product/management/type/spuType' import { PropType } from 'vue' import { copyValueToTarget } from '@/utils/object' import { propTypes } from '@/utils/propTypes' - const message = useMessage() // 消息弹窗 + const props = defineProps({ propFormData: { type: Object as PropType<SpuType>, @@ -63,7 +64,7 @@ const props = defineProps({ }, activeName: propTypes.string.def('') }) -// 商品推荐选项 +// 商品推荐选项 TODO @puhui999:这种叫 recommendOptions 会更合适哈 const recommend = [ { name: '是否热卖', value: 'recommendHot' }, { name: '是否优惠', value: 'recommendBenefit' }, @@ -71,10 +72,10 @@ const recommend = [ { name: '是否新品', value: 'recommendNew' }, { name: '是否优品', value: 'recommendGood' } ] -// 选中推荐选项 -const checkboxGroup = ref<string[]>(['recommendHot']) -// 选择商品后赋值 +const checkboxGroup = ref<string[]>(['recommendHot']) // 选中推荐选项 +/** 选择商品后赋值 */ const onChangeGroup = () => { + // TODO @puhui999:是不是可以遍历 recommend,然后进行是否选中; checkboxGroup.value.includes('recommendHot') ? (formData.value.recommendHot = true) : (formData.value.recommendHot = false) @@ -109,6 +110,7 @@ const rules = reactive({ giveIntegral: [required], virtualSalesCount: [required] }) + /** * 将传进来的值赋值给formData */ @@ -130,10 +132,11 @@ watch( immediate: true } ) -const emit = defineEmits(['update:activeName']) + /** * 表单校验 */ +const emit = defineEmits(['update:activeName']) const validate = async () => { // 校验表单 if (!OtherSettingsFormRef) return @@ -149,6 +152,5 @@ const validate = async () => { } }) } - defineExpose({ validate }) </script> diff --git a/src/views/mall/product/management/components/ProductAttributes.vue b/src/views/mall/product/spu/components/ProductAttributes.vue similarity index 99% rename from src/views/mall/product/management/components/ProductAttributes.vue rename to src/views/mall/product/spu/components/ProductAttributes.vue index 2283f483..73e8c992 100644 --- a/src/views/mall/product/management/components/ProductAttributes.vue +++ b/src/views/mall/product/spu/components/ProductAttributes.vue @@ -71,16 +71,19 @@ watch( immediate: true } ) + /** 删除标签 tagValue 标签值*/ const handleClose = (index, valueIndex) => { attributeList.value[index].values?.splice(valueIndex, 1) } + /** 显示输入框并获取焦点 */ const showInput = async (index) => { attributeIndex.value = index // 因为组件在ref中所以需要用索引获取对应的Ref InputRef.value[index]!.input!.focus() } + /** 输入框失去焦点或点击回车时触发 */ const handleInputConfirm = async (index, propertyId) => { if (inputValue.value) { diff --git a/src/views/mall/product/management/components/ProductAttributesAddForm.vue b/src/views/mall/product/spu/components/ProductAttributesAddForm.vue similarity index 100% rename from src/views/mall/product/management/components/ProductAttributesAddForm.vue rename to src/views/mall/product/spu/components/ProductAttributesAddForm.vue diff --git a/src/views/mall/product/management/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue similarity index 95% rename from src/views/mall/product/management/components/SkuList.vue rename to src/views/mall/product/spu/components/SkuList.vue index 3311202a..9e1c666f 100644 --- a/src/views/mall/product/management/components/SkuList.vue +++ b/src/views/mall/product/spu/components/SkuList.vue @@ -25,11 +25,13 @@ </template> </el-table-column> </template> + <!-- TODO @puhui999: controls-position="right" 可以去掉哈,不然太长了,手动输入更方便 --> <el-table-column align="center" label="商品条码" min-width="168"> <template #default="{ row }"> <el-input v-model="row.barCode" class="w-100%" /> </template> </el-table-column> + <!-- TODO @puhui999:用户输入的时候,是按照元;分主要是我们自己用; --> <el-table-column align="center" label="销售价(分)" min-width="168"> <template #default="{ row }"> <el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" /> @@ -94,15 +96,14 @@ </template> <el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80"> <template #default> - <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd" - >批量添加 + <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd"> + 批量添加 </el-button> <el-button v-else link size="small" type="primary">删除</el-button> </template> </el-table-column> </el-table> </template> - <script lang="ts" name="SkuList" setup> import { UploadImg } from '@/components/UploadFile' import { PropType } from 'vue' @@ -123,7 +124,7 @@ const props = defineProps({ isBatch: propTypes.bool.def(false) // 是否批量操作 }) const formData = ref<SpuType>() // 表单数据 -// 批量添加时的零时数据 +// 批量添加时的零时数据 TODO @puhui999:小写开头哈;然后变量都尾注释 const SkuData = ref<SkuType[]>([ { /** @@ -168,13 +169,16 @@ const SkuData = ref<SkuType[]>([ subCommissionSecondPrice: 0 } ]) + /** 批量添加 */ const batchAdd = () => { formData.value.skus.forEach((item) => { copyValueToTarget(item, SkuData.value[0]) }) } + const tableHeaderList = ref<{ prop: string; label: string }[]>([]) + /** * 将传进来的值赋值给SkuData */ @@ -189,6 +193,8 @@ watch( immediate: true } ) + +// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 /** 生成表数据 */ const generateTableData = (data: any[]) => { // 构建数据结构 @@ -237,6 +243,7 @@ const generateTableData = (data: any[]) => { formData.value.skus.push(row) }) } + /** 构建所有排列组合 */ const build = (list: any[]) => { if (list.length === 0) { @@ -259,6 +266,7 @@ const build = (list: any[]) => { return result } } + /** 监听属性列表生成相关参数和表头 */ watch( () => props.attributeList, diff --git a/src/views/mall/product/management/components/index.ts b/src/views/mall/product/spu/components/index.ts similarity index 100% rename from src/views/mall/product/management/components/index.ts rename to src/views/mall/product/spu/components/index.ts diff --git a/src/views/mall/product/management/index.vue b/src/views/mall/product/spu/index.vue similarity index 89% rename from src/views/mall/product/management/index.vue rename to src/views/mall/product/spu/index.vue index 7b5fb0c1..b3a04c88 100644 --- a/src/views/mall/product/management/index.vue +++ b/src/views/mall/product/spu/index.vue @@ -8,6 +8,7 @@ class="-mb-15px" label-width="68px" > + <!-- TODO @puhui999:https://admin.java.crmeb.net/store/index,参考,使用分类 + 标题搜索 --> <el-form-item label="品牌名称" prop="name"> <el-input v-model="queryParams.name" @@ -51,6 +52,7 @@ <Icon class="mr-5px" icon="ep:plus" /> 新增 </el-button> + <!-- TODO @puhui999:增加一个【导出】操作 --> </el-form-item> </el-form> </ContentWrap> @@ -66,6 +68,7 @@ /> </el-tabs> <el-table v-loading="loading" :data="list"> + <!-- TODO puhui999: ID 编号的展示 --> <!-- TODO 暂时不做折叠数据 --> <!-- <el-table-column type="expand">--> <!-- <template #default="{ row }">--> @@ -92,6 +95,7 @@ </template> </el-table-column> <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" /> + <!-- TODO 价格 / 100.0 --> <el-table-column align="center" label="商品售价" min-width="90" prop="price" /> <el-table-column align="center" label="销量" min-width="90" prop="salesCount" /> <el-table-column align="center" label="库存" min-width="90" prop="stock" /> @@ -105,6 +109,7 @@ /> <el-table-column fixed="right" label="状态" min-width="80"> <template #default="{ row }"> + <!-- TODO @puhui:是不是不用 Number(row.status) 去比较哈,直接 row.status < 0 --> <el-switch v-model="row.status" :active-value="1" @@ -119,6 +124,7 @@ </el-table-column> <el-table-column align="center" fixed="right" label="操作" min-width="150"> <template #default="{ row }"> + <!-- TODO @puhui999:【详情】,可以后面点做哈 --> <template v-if="queryParams.tabType === 4"> <el-button v-hasPermi="['product:spu:delete']" @@ -166,6 +172,7 @@ @pagination="getList" /> </ContentWrap> + <!-- https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html,可以用这个么? --> <!-- 必须在表格外面展示。不然单元格会遮挡图层 --> <el-image-viewer v-if="imgViewVisible" @@ -173,20 +180,21 @@ @close="imgViewVisible = false" /> </template> -<script lang="ts" name="ProductManagement" setup> +<script lang="ts" name="ProductList" setup> import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' // 业务api +import { dateFormatter } from '@/utils/formatTime' +// TODO @puhui999:managementApi=》ProductSpuApi import * as managementApi from '@/api/mall/product/management/spu' import { ProductSpuStatusEnum } from '@/utils/constants' import { TabsPaneContext } from 'element-plus' - const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 const { currentRoute, push } = useRouter() // 路由跳转 + const loading = ref(false) // 列表的加载中 const total = ref(0) // 列表的总页数 const list = ref<any[]>([]) // 列表的数据 -// tabs数据 +// tabs 数据 const tabsData = ref([ { count: 0, @@ -214,7 +222,10 @@ const tabsData = ref([ type: 4 } ]) + +/** 获得每个 Tab 的数量 */ const getTabsCount = async () => { + // TODO @puhui999:这里是不是可以不要 try catch 哈 try { const res = await managementApi.getTabsCount() for (let objName in res) { @@ -222,6 +233,7 @@ const getTabsCount = async () => { } } catch {} } + const imgViewVisible = ref(false) // 商品图预览 const imageViewerList = ref<string[]>([]) // 商品图预览列表 const queryParams = ref({ @@ -230,10 +242,13 @@ const queryParams = ref({ tabType: 0 }) const queryFormRef = ref() // 搜索的表单 + +// TODO @puhui999:可以改成 handleTabClick:更准确一点; const handleClick = (tab: TabsPaneContext) => { queryParams.value.tabType = tab.paneName getList() } + /** 查询列表 */ const getList = async () => { loading.value = true @@ -246,8 +261,10 @@ const getList = async () => { } } +// TODO @puhui999:是不是 changeStatus 和 addToTrash 调用一个统一的方法,去更新状态。这样逻辑会更干净一些。 /** - * 更改SPU状态 + * 更改 SPU 状态 + * * @param row * @param status 更改前的值 */ @@ -271,7 +288,7 @@ const changeStatus = async (row, status?: number) => { ) await managementApi.updateStatus({ id: row.id, status: row.status }) message.success('更新状态成功') - // 刷新tabs数据 + // 刷新 tabs 数据 await getTabsCount() // 刷新列表 await getList() @@ -288,8 +305,10 @@ const changeStatus = async (row, status?: number) => { : ProductSpuStatusEnum.DISABLE.status } } + /** * 加入回收站 + * * @param row * @param status */ @@ -299,6 +318,7 @@ const addToTrash = (row, status) => { row.status = status changeStatus(row, num) } + /** 删除按钮操作 */ const handleDelete = async (id: number) => { try { @@ -313,6 +333,7 @@ const handleDelete = async (id: number) => { await getList() } catch {} } + /** * 商品图预览 * @param imgUrl @@ -321,6 +342,7 @@ const imagePreview = (imgUrl: string) => { imageViewerList.value = [imgUrl] imgViewVisible.value = true } + /** 搜索按钮操作 */ const handleQuery = () => { getList() @@ -334,16 +356,20 @@ const resetQuery = () => { /** * 新增或修改 - * @param id + * + * @param id 商品 SPU 编号 */ const openForm = (id?: number) => { + // 修改 if (typeof id === 'number') { push('/product/productManagementAdd?id=' + id) return } + // 新增 push('/product/productManagementAdd') } -// 监听路由变化更新列表 + +// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么? watch( () => currentRoute.value, () => { @@ -353,6 +379,7 @@ watch( immediate: true } ) + /** 初始化 **/ onMounted(() => { getTabsCount() From bf629a95549757faa90362b5d3c0c6a2e6a443a4 Mon Sep 17 00:00:00 2001 From: Chika <wbs_2018@sina.com> Date: Fri, 12 May 2023 15:30:01 +0800 Subject: [PATCH 26/28] =?UTF-8?q?Redis=E7=9B=91=E6=8E=A7=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E4=BD=BF=E7=94=A8Echart=E7=BB=84=E4=BB=B6=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/modules/remaining.ts | 22 +++ src/views/infra/redis/index.vue | 279 +++++++++++++++++++++----------- 2 files changed, 209 insertions(+), 92 deletions(-) diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 8886e388..ed0d6720 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -318,6 +318,28 @@ const remainingRouter: AppRouteRecordRaw[] = [ } ] }, + { + path: '/infra', + component: Layout, + name: 'InfraRedis', + meta: { + hidden: true + }, + children: [ + { + path: '/infra/redis', + component: () => import('@/views/infra/redis/index.vue'), + name: 'InfraRedis', + meta: { + noCache: true, + hidden: true, + canTo: true, + title: 'REDIS测试测试测试', + activeMenu: 'infra/redis/index' + } + } + ] + }, { path: '/property', component: Layout, diff --git a/src/views/infra/redis/index.vue b/src/views/infra/redis/index.vue index 246b90ee..948148ad 100644 --- a/src/views/infra/redis/index.vue +++ b/src/views/infra/redis/index.vue @@ -1,7 +1,6 @@ <template> <doc-alert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" /> <doc-alert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" /> - <el-scrollbar height="calc(100vh - 88px - 40px - 50px)"> <el-row> <!-- 基本信息 --> @@ -51,126 +50,222 @@ <!-- 命令统计 --> <el-col :span="12" class="mt-3"> <el-card :gutter="12" shadow="hover"> - <div ref="commandStatsRef" class="h-88"></div> + <Echart :options="commandStatsRefChika" :height="420" /> </el-card> </el-col> <!-- 内存使用量统计 --> <el-col :span="12" class="mt-3"> <el-card class="ml-3" :gutter="12" shadow="hover"> - <div ref="usedmemory" class="h-88"></div> + <Echart :options="usedmemoryEchartChika" :height="420" /> </el-card> </el-col> </el-row> </el-scrollbar> </template> -<script setup lang="ts" name="InfraRedis"> -import * as echarts from 'echarts' +<script setup lang="ts"> +import echarts from '@/plugins/echarts' +import { GaugeChart } from 'echarts/charts' +import { ToolboxComponent } from 'echarts/components' import * as RedisApi from '@/api/infra/redis' import { RedisMonitorInfoVO } from '@/api/infra/redis/types' - +echarts.use([ToolboxComponent]) +echarts.use([GaugeChart]) const cache = ref<RedisMonitorInfoVO>() // 基本信息 const readRedisInfo = async () => { const data = await RedisApi.getCache() cache.value = data - loadEchartOptions(data.commandStats) } -// 图表 -const commandStatsRef = ref<HTMLElement>() -const usedmemory = ref<HTMLDivElement>() -const loadEchartOptions = (stats) => { - const commandStats = [] as any[] - const nameList = [] as string[] - stats.forEach((row) => { - commandStats.push({ - name: row.command, - value: row.calls - }) - nameList.push(row.command) - }) - - const commandStatsInstance = echarts.init(commandStatsRef.value!, 'macarons') - - commandStatsInstance.setOption({ - title: { - text: '命令统计', - left: 'center' - }, - tooltip: { - trigger: 'item', - formatter: '{a} <br/>{b} : {c} ({d}%)' - }, - legend: { - type: 'scroll', - orient: 'vertical', - right: 30, - top: 10, - bottom: 20, - data: nameList, - textStyle: { - color: '#a1a1a1' +// 内存使用情况 +const usedmemoryEchartChika = reactive({ + title: { + // 仪表盘标题。 + text: '内存使用情况', + left: 'center', + show: true, // 是否显示标题,默认 true。 + offsetCenter: [0, '20%'], //相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。 + color: 'yellow', // 文字的颜色,默认 #333。 + fontSize: 20 // 文字的字体大小,默认 15。 + }, + toolbox: { + show: false, + feature: { + restore: { show: true }, + saveAsImage: { show: true } + } + }, + series: [ + { + name: '峰值', + type: 'gauge', + min: 0, + max: 50, + splitNumber: 10, + //这是指针的颜色 + color: '#F5C74E', + radius: '85%', + center: ['50%', '50%'], + startAngle: 225, + endAngle: -45, + axisLine: { + // 坐标轴线 + lineStyle: { + // 属性lineStyle控制线条样式 + color: [ + [0.2, '#7FFF00'], + [0.8, '#00FFFF'], + [1, '#FF0000'] + ], + //width: 6 外框的大小(环的宽度) + width: 10 + } + }, + axisTick: { + // 坐标轴小标记 + //里面的线长是5(短线) + length: 5, // 属性length控制线长 + lineStyle: { + // 属性lineStyle控制线条样式 + color: '#76D9D7' + } + }, + splitLine: { + // 分隔线 + length: 20, // 属性length控制线长 + lineStyle: { + // 属性lineStyle(详见lineStyle)控制线条样式 + color: '#76D9D7' + } + }, + axisLabel: { + color: '#76D9D7', + distance: 15, + fontSize: 15 + }, + pointer: { + //指针的大小 + width: 7, + show: true + }, + detail: { + textStyle: { + fontWeight: 'normal', + //里面文字下的数值大小(50) + fontSize: 15, + color: '#FFFFFF' + }, + valueAnimation: true + }, + progress: { + show: true } - }, - series: [ - { - name: '命令', - type: 'pie', - radius: [20, 120], - center: ['40%', '60%'], - data: commandStats, - roseType: 'radius', + } + ] +}) + +// 指令使用情况 +const commandStatsRefChika = reactive({ + title: { + text: '命令统计', + left: 'center' + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b} : {c} ({d}%)' + }, + legend: { + type: 'scroll', + orient: 'vertical', + right: 30, + top: 10, + bottom: 20, + data: [] as any[], + textStyle: { + color: '#a1a1a1' + } + }, + series: [ + { + name: '命令', + type: 'pie', + radius: [20, 120], + center: ['40%', '60%'], + data: [] as any[], + roseType: 'radius', + label: { + show: true + }, + emphasis: { label: { show: true }, - emphasis: { - label: { - show: true - }, - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' } } - ] - }) + } + ] +}) - const usedMemoryInstance = echarts.init(usedmemory.value!, 'macarons') - usedMemoryInstance.setOption({ - title: { - text: '内存使用情况', - left: 'center' - }, - tooltip: { - formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human - }, - series: [ - { - name: '峰值', - type: 'gauge', - min: 0, - max: 100, - progress: { - show: true - }, - detail: { - formatter: cache.value!.info.used_memory_human - }, - data: [ - { - value: parseFloat(cache.value!.info.used_memory_human), - name: '内存消耗' - } - ] - } - ] - }) +/** 加载数据 */ +const getSummary = () => { + //初始化命令图表 + initcommandStatsChart() + usedMemoryInstance() } -onBeforeMount(() => { +/** 命令使用情况 */ +const initcommandStatsChart = async () => { + usedmemoryEchartChika.series[0].data = [] + // 发起请求 + try { + const data = await RedisApi.getCache() + cache.value = data + // 处理数据 + const commandStats = [] as any[] + const nameList = [] as string[] + data.commandStats.forEach((row) => { + commandStats.push({ + name: row.command, + value: row.calls + }) + nameList.push(row.command) + }) + commandStatsRefChika.legend.data = nameList + commandStatsRefChika.series[0].data = commandStats + } catch {} +} +const usedMemoryInstance = async () => { + try { + const data = await RedisApi.getCache() + cache.value = data + // 仪表盘详情,用于显示数据。 + usedmemoryEchartChika.series[0].detail = { + show: true, // 是否显示详情,默认 true。 + offsetCenter: [0, '50%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。 + color: 'auto', // 文字的颜色,默认 auto。 + fontSize: 30, // 文字的字体大小,默认 15。 + formatter: cache.value!.info.used_memory_human // 格式化函数或者字符串 + } + + usedmemoryEchartChika.series[0].data[0] = { + value: cache.value!.info.used_memory_human, + name: '内存消耗' + } + usedmemoryEchartChika.tooltip = { + formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human + } + } catch {} +} + +/** 初始化 **/ +onMounted(() => { readRedisInfo() + // 加载数据 + getSummary() }) </script> From fbac522149c82db3c4a23691140b0370615a3329 Mon Sep 17 00:00:00 2001 From: Chika <wbs_2018@sina.com> Date: Fri, 12 May 2023 16:10:14 +0800 Subject: [PATCH 27/28] =?UTF-8?q?Redis=E7=9B=91=E6=8E=A7=E7=94=A8Echart?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=95=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/infra/redis/index.vue | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/views/infra/redis/index.vue b/src/views/infra/redis/index.vue index 737d9ca2..0d68c88f 100644 --- a/src/views/infra/redis/index.vue +++ b/src/views/infra/redis/index.vue @@ -218,7 +218,6 @@ const getSummary = () => { usedMemoryInstance() } -<<<<<<< HEAD /** 命令使用情况 */ const initcommandStatsChart = async () => { usedmemoryEchartChika.series[0].data = [] @@ -257,6 +256,7 @@ const usedMemoryInstance = async () => { value: cache.value!.info.used_memory_human, name: '内存消耗' } + console.log(cache.value!.info) usedmemoryEchartChika.tooltip = { formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human } @@ -265,10 +265,6 @@ const usedMemoryInstance = async () => { /** 初始化 **/ onMounted(() => { -======= -onBeforeMount(() => { - // TODO @hiiwbs 微信,优化使用 Echart 组件 ->>>>>>> e92361ed401771998c6abd6d9ba53801a6e0cfa6 readRedisInfo() // 加载数据 getSummary() From 3feb4828cd9bd62e4e365231cf71409bc91951a5 Mon Sep 17 00:00:00 2001 From: YunaiV <zhijiantianya@gmail.com> Date: Mon, 15 May 2023 22:57:31 +0800 Subject: [PATCH 28/28] =?UTF-8?q?!148=20=E3=80=90=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E3=80=91Vue3=20=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=EF=BC=9A[?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=AE=A1=E7=90=86=20->=20Redis=E7=9B=91?= =?UTF-8?q?=E6=8E=A7]=20=E4=BD=BF=E7=94=A8Echart=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/modules/remaining.ts | 22 ---------------------- src/views/infra/redis/index.vue | 15 ++++++++------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 6cc8746b..4f5a16bd 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -330,28 +330,6 @@ const remainingRouter: AppRouteRecordRaw[] = [ } ] }, - { - path: '/infra', - component: Layout, - name: 'InfraRedis', - meta: { - hidden: true - }, - children: [ - { - path: '/infra/redis', - component: () => import('@/views/infra/redis/index.vue'), - name: 'InfraRedis', - meta: { - noCache: true, - hidden: true, - canTo: true, - title: 'REDIS测试测试测试', - activeMenu: 'infra/redis/index' - } - } - ] - }, { path: '/property', component: Layout, diff --git a/src/views/infra/redis/index.vue b/src/views/infra/redis/index.vue index 0d68c88f..011f8e59 100644 --- a/src/views/infra/redis/index.vue +++ b/src/views/infra/redis/index.vue @@ -68,8 +68,6 @@ import { GaugeChart } from 'echarts/charts' import { ToolboxComponent } from 'echarts/components' import * as RedisApi from '@/api/infra/redis' import { RedisMonitorInfoVO } from '@/api/infra/redis/types' -echarts.use([ToolboxComponent]) -echarts.use([GaugeChart]) const cache = ref<RedisMonitorInfoVO>() // 基本信息 @@ -145,14 +143,14 @@ const usedmemoryEchartChika = reactive({ fontSize: 15 }, pointer: { - //指针的大小 + // 指针的大小 width: 7, show: true }, detail: { textStyle: { fontWeight: 'normal', - //里面文字下的数值大小(50) + // 里面文字下的数值大小(50) fontSize: 15, color: '#FFFFFF' }, @@ -213,13 +211,13 @@ const commandStatsRefChika = reactive({ /** 加载数据 */ const getSummary = () => { - //初始化命令图表 - initcommandStatsChart() + // 初始化命令图表 + initCommandStatsChart() usedMemoryInstance() } /** 命令使用情况 */ -const initcommandStatsChart = async () => { +const initCommandStatsChart = async () => { usedmemoryEchartChika.series[0].data = [] // 发起请求 try { @@ -265,6 +263,9 @@ const usedMemoryInstance = async () => { /** 初始化 **/ onMounted(() => { + echarts.use([ToolboxComponent]) + echarts.use([GaugeChart]) + // 读取 redis 信息 readRedisInfo() // 加载数据 getSummary()