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()