From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/mp/menu/index.vue |  403 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 403 insertions(+), 0 deletions(-)

diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue
new file mode 100644
index 0000000..b86286a
--- /dev/null
+++ b/src/views/mp/menu/index.vue
@@ -0,0 +1,403 @@
+<template>
+  <doc-alert title="鍏紬鍙疯彍鍗�" url="https://doc.iocoder.cn/mp/menu/" />
+  <!-- 鎼滅储宸ヤ綔鏍� -->
+  <ContentWrap>
+    <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
+      <el-form-item label="鍏紬鍙�" prop="accountId">
+        <WxAccountSelect @change="onAccountChanged" />
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <div class="clearfix public-account-management" v-loading="loading">
+      <!--宸﹁竟閰嶇疆鑿滃崟-->
+      <div class="left">
+        <div class="weixin-hd">
+          <div class="weixin-title">{{ accountName }}</div>
+        </div>
+        <div class="clearfix weixin-menu">
+          <MenuPreviewer
+            v-model="menuList"
+            :account-id="accountId"
+            :active-index="activeIndex"
+            :parent-index="parentIndex"
+            @menu-clicked="(parent, x) => menuClicked(parent, x)"
+            @submenu-clicked="(child, x, y) => subMenuClicked(child, x, y)"
+          />
+        </div>
+        <div class="save_div">
+          <el-button class="save_btn" type="success" @click="onSave" v-hasPermi="['mp:menu:save']"
+            >淇濆瓨骞跺彂甯冭彍鍗�</el-button
+          >
+          <el-button class="save_btn" type="danger" @click="onClear" v-hasPermi="['mp:menu:delete']"
+            >娓呯┖鑿滃崟</el-button
+          >
+        </div>
+      </div>
+      <!--鍙宠竟閰嶇疆-->
+      <div class="right" v-if="showRightPanel">
+        <MenuEditor
+          :account-id="accountId"
+          :is-parent="isParent"
+          v-model="activeMenu"
+          @delete="onDeleteMenu"
+        />
+      </div>
+      <!-- 涓�杩涢〉闈㈠氨鏄剧ず鐨勯粯璁ら〉闈紝褰撶偣鍑诲乏杈规寜閽殑鏃跺�欙紝灏变笉鏄剧ず浜�-->
+      <div v-else class="right">
+        <p>璇烽�夋嫨鑿滃崟閰嶇疆</p>
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import MenuEditor from './components/MenuEditor.vue'
+import MenuPreviewer from './components/MenuPreviewer.vue'
+import * as MpMenuApi from '@/api/mp/menu'
+import * as UtilsTree from '@/utils/tree'
+import { RawMenu, Menu } from './components/types'
+
+defineOptions({ name: 'MpMenu' })
+
+const message = useMessage() // 娑堟伅
+const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
+
+// ======================== 鍒楄〃鏌ヨ ========================
+const loading = ref(false) // 閬僵灞�
+const accountId = ref(-1)
+const accountName = ref<string>('')
+const menuList = ref<Menu[]>([])
+
+// ======================== 鑿滃崟鎿嶄綔 ========================
+// 褰撳墠閫変腑鑿滃崟缂栫爜锛�
+//  * 涓�绾э紙'x'锛�
+//  * 浜岀骇锛�'x-y'锛�
+//  * 鏈�変腑锛圡ENU_NOT_SELECTED锛�
+const activeIndex = ref<string>(MENU_NOT_SELECTED)
+// 浜岀骇鑿滃崟鏄剧ず鏍囧織: 褰掑睘鐨勪竴绾ц彍鍗昳ndex
+// * 鏈垵濮嬪寲锛�-1
+// * 鍒濆鍖栵細x
+const parentIndex = ref(-1)
+
+// ======================== 鑿滃崟缂栬緫 ========================
+const showRightPanel = ref(false) // 鍙宠竟閰嶇疆鏄剧ず榛樿璇︽儏杩樻槸閰嶇疆璇︽儏
+const isParent = ref<boolean>(true) // 鏄惁涓�绾ц彍鍗曪紝鎺у埗MenuEditor涓璶ame瀛楁闀垮害
+const activeMenu = ref<Menu>({}) // 閫変腑鑿滃崟锛孧enuEditor鐨刴odelValue
+
+// 涓�浜涗复鏃跺�兼斁鍦ㄨ繖閲岃繘琛屽垽鏂紝濡傛灉鏀惧湪 activeMenu锛岀敱浜庡紩鐢ㄥ叧绯伙紝menu 涔熶細澶氫簡澶氫綑鐨勫弬鏁�
+enum Level {
+  Undefined = '0',
+  Parent = '1',
+  Child = '2'
+}
+const tempSelfObj = ref<{
+  grand: Level
+  x: number
+  y: number
+}>({
+  grand: Level.Undefined,
+  x: 0,
+  y: 0
+})
+const dialogNewsVisible = ref(false) // 璺宠浆鍥炬枃鏃剁殑绱犳潗閫夋嫨寮圭獥
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number, name: string) => {
+  accountId.value = id
+  accountName.value = name
+  getList()
+}
+
+/** 鏌ヨ骞惰浆鎹㈣彍鍗� **/
+const getList = async () => {
+  loading.value = false
+  try {
+    const data = await MpMenuApi.getMenuList(accountId.value)
+    const menuData = menuListToFrontend(data)
+    menuList.value = UtilsTree.handleTree(menuData, 'id')
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+  resetForm()
+  getList()
+}
+
+// 灏嗗悗绔繑鍥炵殑 menuList锛岃浆鎹㈡垚鍓嶇鐨� menuList
+const menuListToFrontend = (list: any[]) => {
+  if (!list) return []
+
+  const result: RawMenu[] = []
+  list.forEach((item: RawMenu) => {
+    const menu: any = {
+      ...item
+    }
+    menu.reply = {
+      type: item.replyMessageType,
+      accountId: item.accountId,
+      content: item.replyContent,
+      mediaId: item.replyMediaId,
+      url: item.replyMediaUrl,
+      title: item.replyTitle,
+      description: item.replyDescription,
+      thumbMediaId: item.replyThumbMediaId,
+      thumbMediaUrl: item.replyThumbMediaUrl,
+      articles: item.replyArticles,
+      musicUrl: item.replyMusicUrl,
+      hqMusicUrl: item.replyHqMusicUrl
+    }
+    result.push(menu as RawMenu)
+  })
+  return result
+}
+
+// 閲嶇疆琛ㄥ崟锛屾竻绌鸿〃鍗曟暟鎹�
+const resetForm = () => {
+  // 鑿滃崟鎿嶄綔
+  activeIndex.value = MENU_NOT_SELECTED
+  parentIndex.value = -1
+
+  // 鑿滃崟缂栬緫
+  showRightPanel.value = false
+  activeMenu.value = {}
+  tempSelfObj.value = { grand: Level.Undefined, x: 0, y: 0 }
+  dialogNewsVisible.value = false
+}
+
+// ======================== 鑿滃崟鎿嶄綔 ========================
+// 涓�绾ц彍鍗曠偣鍑讳簨浠�
+const menuClicked = (parent: Menu, x: number) => {
+  // 鍙充晶鐨勮〃鍗曠浉鍏�
+  showRightPanel.value = true // 鍙宠竟鑿滃崟
+  activeMenu.value = parent // 杩欎釜濡傛灉鏀惧湪椤堕儴锛宖lag 浼氭病鏈夈�傚洜涓洪噸鏂拌祴鍊间簡銆�
+  tempSelfObj.value.grand = Level.Parent // 琛ㄧず涓�绾ц彍鍗�
+  tempSelfObj.value.x = x // 琛ㄧず涓�绾ц彍鍗曠储寮�
+  isParent.value = true
+
+  // 宸︿晶鐨勯�変腑
+  activeIndex.value = `${x}` // 鑿滃崟閫変腑鏍峰紡
+  parentIndex.value = x // 浜岀骇鑿滃崟鏄剧ず鏍囧織
+}
+
+// 浜岀骇鑿滃崟鐐瑰嚮浜嬩欢
+const subMenuClicked = (child: Menu, x: number, y: number) => {
+  // 鍙充晶鐨勮〃鍗曠浉鍏�
+  showRightPanel.value = true // 鍙宠竟鑿滃崟
+  activeMenu.value = child // 灏嗙偣鍑荤殑鏁版嵁鏀惧埌涓存椂鍙橀噺锛屽璞℃湁寮曠敤浣滅敤
+  tempSelfObj.value.grand = Level.Child // 琛ㄧず浜岀骇鑿滃崟
+  tempSelfObj.value.x = x // 琛ㄧず涓�绾ц彍鍗曠储寮�
+  tempSelfObj.value.y = y // 琛ㄧず浜岀骇鑿滃崟绱㈠紩
+  isParent.value = false
+
+  // 宸︿晶鐨勯�変腑
+  activeIndex.value = `${x}-${y}`
+}
+
+// 鍒犻櫎褰撳墠鑿滃崟
+const onDeleteMenu = async () => {
+  try {
+    await message.confirm('纭畾瑕佸垹闄ゅ悧?')
+    if (tempSelfObj.value.grand === Level.Parent) {
+      // 涓�绾ц彍鍗曠殑鍒犻櫎鏂规硶
+      menuList.value.splice(tempSelfObj.value.x, 1)
+    } else if (tempSelfObj.value.grand === Level.Child) {
+      // 浜岀骇鑿滃崟鐨勫垹闄ゆ柟娉�
+      menuList.value[tempSelfObj.value.x].children?.splice(tempSelfObj.value.y, 1)
+    }
+    // 鎻愮ず
+    message.notifySuccess('鍒犻櫎鎴愬姛')
+
+    // 澶勭悊鑿滃崟鐨勯�変腑
+    activeMenu.value = {}
+    showRightPanel.value = false
+    activeIndex.value = MENU_NOT_SELECTED
+  } catch {
+    //
+  }
+}
+
+// ======================== 鑿滃崟缂栬緫 ========================
+const onSave = async () => {
+  try {
+    await message.confirm('纭畾瑕佷繚瀛樺悧?')
+    loading.value = true
+    await MpMenuApi.saveMenu(accountId.value, menuListToBackend())
+    getList()
+    message.notifySuccess('鍙戝竷鎴愬姛')
+  } finally {
+    loading.value = false
+  }
+}
+
+const onClear = async () => {
+  try {
+    await message.confirm('纭畾瑕佸垹闄ゅ悧?')
+    loading.value = true
+    await MpMenuApi.deleteMenu(accountId.value)
+    handleQuery()
+    message.notifySuccess('娓呯┖鎴愬姛')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 灏嗗墠绔殑 menuList锛岃浆鎹㈡垚鍚庣鎺ユ敹鐨� menuList
+const menuListToBackend = () => {
+  const result: any[] = []
+  menuList.value.forEach((item) => {
+    const menu = menuToBackend(item)
+    result.push(menu)
+
+    // 澶勭悊瀛愯彍鍗�
+    if (!item.children || item.children.length <= 0) {
+      return
+    }
+    menu.children = []
+    item.children.forEach((subItem) => {
+      menu.children.push(menuToBackend(subItem))
+    })
+  })
+  return result
+}
+
+// 灏嗗墠绔殑 menu锛岃浆鎹㈡垚鍚庣鎺ユ敹鐨� menu
+// TODO: @鑺嬭壙锛岄渶瑕佹牴鎹悗鍙癆PI鍒犻櫎涓嶉渶瑕佺殑瀛楁
+const menuToBackend = (menu: any) => {
+  const result = {
+    ...menu,
+    children: undefined, // 涓嶅鐞嗗瓙鑺傜偣
+    reply: undefined // 绋嶅悗澶嶅埗
+  }
+  result.replyMessageType = menu.reply.type
+  result.replyContent = menu.reply.content
+  result.replyMediaId = menu.reply.mediaId
+  result.replyMediaUrl = menu.reply.url
+  result.replyTitle = menu.reply.title
+  result.replyDescription = menu.reply.description
+  result.replyThumbMediaId = menu.reply.thumbMediaId
+  result.replyThumbMediaUrl = menu.reply.thumbMediaUrl
+  result.replyArticles = menu.reply.articles
+  result.replyMusicUrl = menu.reply.musicUrl
+  result.replyHqMusicUrl = menu.reply.hqMusicUrl
+
+  return result
+}
+</script>
+
+<!--鏈粍浠舵牱寮�-->
+<style lang="scss" scoped="scoped">
+/* 鍏叡棰滆壊鍙橀噺 */
+.clearfix {
+  *zoom: 1;
+}
+
+.clearfix::after {
+  display: table;
+  clear: both;
+  content: '';
+}
+
+div {
+  text-align: left;
+}
+
+.weixin-hd {
+  position: relative;
+  bottom: 426px;
+  left: 0;
+  width: 300px;
+  height: 64px;
+  color: #fff;
+  text-align: center;
+  background: transparent url('./assets/menu_head.png') no-repeat 0 0;
+  background-position: 0 0;
+  background-size: 100%;
+}
+
+.weixin-title {
+  position: absolute;
+  top: 33px;
+  left: 0;
+  width: 100%;
+  font-size: 14px;
+  color: #fff;
+  text-align: center;
+}
+
+.weixin-menu {
+  padding-left: 43px;
+  font-size: 12px;
+  background: transparent url('./assets/menu_foot.png') no-repeat 0 0;
+}
+
+.public-account-management {
+  width: 1200px;
+  // min-width: 1200px;
+  margin: 0 auto;
+
+  .left {
+    position: relative;
+    display: block;
+    float: left;
+    width: 350px;
+    height: 715px;
+    padding: 518px 25px 88px;
+    background: url('./assets/iphone_backImg.png') no-repeat;
+    background-size: 100% auto;
+    box-sizing: border-box;
+
+    .save_div {
+      margin-top: 15px;
+      text-align: center;
+
+      .save_btn {
+        bottom: 20px;
+        left: 100px;
+      }
+    }
+  }
+
+  /* 鍙宠竟鑿滃崟鍐呭 */
+  .right {
+    float: left;
+    width: 63%;
+    padding: 20px;
+    margin-left: 20px;
+    background-color: #e8e7e7;
+    box-sizing: border-box;
+  }
+}
+</style>
+<!--绱犳潗鏍峰紡-->
+<style lang="scss" scoped>
+.pagination {
+  margin-right: 25px;
+  text-align: right;
+}
+
+.select-item {
+  width: 280px;
+  padding: 10px;
+  margin: 0 auto 10px;
+  border: 1px solid #eaeaea;
+}
+
+.ope-row {
+  padding-top: 10px;
+  text-align: center;
+}
+
+.item-name {
+  overflow: hidden;
+  font-size: 12px;
+  text-align: center;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

--
Gitblit v1.8.0