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