useMenuSplit.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { ref, computed, watch, nextTick, onMounted } from 'vue'
  2. import { useI18n } from 'vue-i18n'
  3. import Config from '@/config/index'
  4. import { localesList } from '@/locale/index'
  5. import { useWindowWidth } from '@/composables/useWindowWidth'
  6. import useGlobalStore from '@/stores/use-global-store'
  7. import useRouter from '@/hooks/useRouter'
  8. import useRoute from '@/hooks/useRoute'
  9. import LiveChatService from '@/utils/liveChat.js'
  10. export interface MenuItem {
  11. path: string
  12. label: string
  13. icon: string
  14. children?: MenuItem[]
  15. isOpenMenu?: boolean
  16. submenuHeight?: number
  17. isExternal?: boolean
  18. type?: string
  19. lang?: string
  20. }
  21. function cloneMenu(menus: MenuItem[]): MenuItem[] {
  22. return menus.map(item => ({
  23. ...item,
  24. children: item.children ? cloneMenu(item.children) : [],
  25. isOpenMenu: item.isOpenMenu ?? false,
  26. }))
  27. }
  28. export function useMenuSplit(handleClick1: (item: MenuItem) => void) {
  29. const { locale } = useI18n()
  30. const globalStore = useGlobalStore()
  31. const mode = computed(() => globalStore.mode)
  32. const windowWidth = useWindowWidth(300)
  33. const shouldShowLanguageMenu = computed(() => windowWidth.value <= 991)
  34. const { host } = Config
  35. const router = useRouter()
  36. const route = useRoute()
  37. // 子菜单 DOM 引用
  38. const submenuRefs = ref<any[]>([])
  39. function setSubmenuRef(index: number, el: HTMElement | null) {
  40. if (el) {
  41. submenuRefs.value[index] = el
  42. }
  43. }
  44. // 设置全局模式
  45. function setMode(code: string) {
  46. globalStore.setMode(code);
  47. const homePath = mode.value === 'customer' ? '/pages/customer/index' : '/pages/ib/index'
  48. router.reLaunch(homePath)
  49. if (code === 'ib') {
  50. uni.$emit('open-ib')
  51. }
  52. nextTick(() => {
  53. submenuRefs.value = [] // 重置,等待重新绑定
  54. menus.value.forEach((item, index) => {
  55. if (item.isOpenMenu && item.children && item.children.length) {
  56. updateSubmenuHeight(index)
  57. }
  58. })
  59. })
  60. }
  61. // 测量元素实际高度(用于过渡动画)
  62. const measureHeight = (element: HTMLElement): number => {
  63. const originalDisplay = element.style.display
  64. const originalPosition = element.style.position
  65. const originalVisibility = element.style.visibility
  66. const originalWidth = element.style.width
  67. element.style.display = 'block'
  68. element.style.position = 'absolute'
  69. element.style.visibility = 'hidden'
  70. element.style.width = '100%'
  71. const height = element.scrollHeight || element.offsetHeight
  72. element.style.display = originalDisplay
  73. element.style.position = originalPosition
  74. element.style.visibility = originalVisibility
  75. element.style.width = originalWidth
  76. return height
  77. }
  78. // 更新指定索引的子菜单高度
  79. const updateSubmenuHeight = (index: number) => {
  80. const refs = submenuRefs.value
  81. nextTick(() => {
  82. if (refs && refs[index]) {
  83. const el = refs[index].$el || refs[index]
  84. const height = measureHeight(el)
  85. if (height > 0) {
  86. menus.value[index].submenuHeight = height
  87. }
  88. }
  89. })
  90. }
  91. let clickTimer: ReturnType<typeof setTimeout> | null = null
  92. // 点击菜单项(切换展开/折叠或跳转)
  93. function handleClick(index: number) {
  94. if (clickTimer) return
  95. clickTimer = setTimeout(() => {
  96. clickTimer = null
  97. }, 300)
  98. const item = menus.value[index]
  99. // 无子菜单:执行跳转或特殊操作
  100. if (!item.children || item.children.length === 0) {
  101. // #ifdef H5
  102. if (item.type === 'chat' && !shouldShowLanguageMenu.value) {
  103. if (LiveChatService) {
  104. LiveChatService.showChat();
  105. }
  106. return
  107. } else {
  108. handleClick1?.(item)
  109. router.push(item.path)
  110. return
  111. }
  112. // #endif
  113. handleClick1?.(item)
  114. router.push(item.path)
  115. return
  116. }
  117. // 有子菜单:切换展开/折叠状态
  118. item.isOpenMenu = !item.isOpenMenu
  119. if (item.isOpenMenu) {
  120. nextTick(() => updateSubmenuHeight(index))
  121. }
  122. }
  123. // 子菜单项点击事件(由 cwg-submenu 组件发出)
  124. function handleSubmenuClick(subItem: any) {
  125. // 处理语言切换
  126. if (subItem.type === 'lang') {
  127. locale.value = subItem.lang
  128. return
  129. }
  130. // 处理外部链接
  131. if (subItem.isExternal) {
  132. // #ifdef H5
  133. window.open(subItem.path, '_blank')
  134. // #endif
  135. // #ifdef APP-PLUS
  136. plus.runtime.openURL(subItem.path)
  137. // #endif
  138. return
  139. }
  140. // 内部页面跳转
  141. handleClick1(subItem)
  142. router.push(subItem.path)
  143. }
  144. // 窗口大小变化时重新计算所有已展开子菜单的高度
  145. const handleResize = () => {
  146. menus.value.forEach((item, index) => {
  147. if (item.isOpenMenu && item.children && item.children.length) {
  148. updateSubmenuHeight(index)
  149. }
  150. })
  151. }
  152. const customMenuList = computed(() =>
  153. localesList.map((code) => ({
  154. label: `language.${code}`,
  155. lang: code,
  156. type: "lang",
  157. path: '/'
  158. }))
  159. )
  160. const languageMenuItem = computed<MenuItem>(() => ({
  161. path: '/',
  162. isOpenMenu: false,
  163. label: 'language.index',
  164. icon: 'cwg-lang',
  165. children: customMenuList.value,
  166. submenuHeight: 0,
  167. }))
  168. const customerBaseMenus = computed<MenuItem[]>(() => [
  169. {
  170. isOpenMenu: false,
  171. submenuHeight: 0,
  172. path: '/',
  173. label: 'Shop.Index.Transaction',
  174. icon: 'crm-trade',
  175. children: [
  176. { path: '/pages/customer/index', label: 'Custom.Index.AccountList', icon: 'icon-client' },
  177. { path: '/pages/customer/trade-history', label: 'Ib.Report.Tit1', icon: 'icon-transfer' },
  178. { path: '/pages/customer/trade-position', label: 'Ib.Report.Tit4', icon: 'icon-transfer' },
  179. { path: '/pages/customer/recording-history', label: 'Home.page_customer.item7', icon: 'icon-application' },
  180. ],
  181. },
  182. {
  183. isOpenMenu: false,
  184. submenuHeight: 0,
  185. path: '/',
  186. label: 'vu.item6',
  187. icon: 'crm-payment',
  188. children: [
  189. { path: '/pages/customer/deposit-select', label: 'Home.page_customer.item2', icon: 'icon-deposit' },
  190. { path: '/pages/customer/withdrawal-select', label: 'Home.page_customer.item3', icon: 'icon-withdrawal' },
  191. { path: '/pages/customer/payment-history', label: 'Home.page_customer.item4', icon: 'icon-payment' },
  192. { path: '/pages/customer/transfer', label: 'Custom.Index.Transfer', icon: 'icon-transfer' },
  193. { path: '/pages/customer/wallet-transfer', label: 'wallet.item62', icon: 'icon-transfer' },
  194. { path: '/pages/customer/wallet-history', label: 'wallet.item7', icon: 'icon-transfer' },
  195. ],
  196. },
  197. {
  198. path: '/pages/activities/index',
  199. isOpenMenu: false,
  200. label: 'Home.page_customer.item6',
  201. icon: 'crm-hd',
  202. children: [],
  203. },
  204. {
  205. path: '/',
  206. isOpenMenu: false,
  207. label: 'vu.item5',
  208. icon: 'crm-chart-area',
  209. children: [
  210. { path: '/pages/analytics/analystViews', label: 'News.Announcement', icon: 'icon-application' },
  211. { path: '/pages/analytics/news', label: 'News.NewsInformation', icon: 'icon-application' },
  212. // {
  213. // path: `https://www.${host}.com/${locale.value}/economic-calendar`,
  214. // label: 'News.FinancialCalendar',
  215. // icon: 'icon-application',
  216. // isExternal: true,
  217. // },
  218. ],
  219. },
  220. {
  221. path: '/pages/common/download',
  222. isOpenMenu: false,
  223. label: 'Downloadpage.item1',
  224. icon: 'crm-download',
  225. children: [],
  226. },
  227. {
  228. path: '/pages/common/chat',
  229. isOpenMenu: false,
  230. label: 'Downloadpage.item16',
  231. icon: 'crm-headset',
  232. children: [],
  233. type: 'chat',
  234. },
  235. {
  236. path: '/',
  237. isOpenMenu: false,
  238. label: 'Custom.Index.Settings',
  239. icon: 'crm-sz',
  240. children: [
  241. { path: '/pages/mine/info?type=1', label: 'PersonalManagement.Title.PersonalInformation', icon: 'crm-headset' },
  242. { path: '/pages/mine/info?type=2', label: 'PersonalManagement.Title.BankInformation', icon: 'crm-headset' },
  243. { path: '/pages/mine/info?type=3', label: 'PersonalManagement.Title.FileManagement', icon: 'crm-headset' },
  244. { path: '/pages/mine/info?type=4', label: 'PersonalManagement.Title.SecurityCenter', icon: 'crm-headset' },
  245. { path: '/pages/common/notice', label: 'News.Notice', icon: 'crm-headset' },
  246. ],
  247. },
  248. ])
  249. const ibBaseMenus = computed<MenuItem[]>(() => [
  250. {
  251. isOpenMenu: false,
  252. path: '/pages/ib/index',
  253. label: 'Documentary.console.item1',
  254. icon: 'crm-mb',
  255. },
  256. {
  257. path: '/',
  258. label: 'Ib.Custom.Manage3',
  259. icon: 'crm-bg',
  260. children: [
  261. { path: '/pages/ib/customer', label: 'Ib.Custom.Manage3', icon: 'icon-deposit' },
  262. { path: '/pages/ib/subsList', label: 'Ib.Custom.Manage2', icon: 'icon-deposit' },
  263. { path: '/pages/ib/agentList', label: 'Documentary.console.item23', icon: 'icon-deposit' },
  264. { path: '/pages/ib/accountList', label: 'Ib.Custom.Manage1', icon: 'icon-deposit' }
  265. ],
  266. },
  267. {
  268. isOpenMenu: false,
  269. submenuHeight: 0,
  270. path: '/',
  271. label: 'vu.item6',
  272. icon: 'crm-payment',
  273. children: [
  274. { path: '/pages/ib/transfer', label: 'Home.page_ib.item4', icon: 'icon-payment' },
  275. { path: '/pages/ib/withdraw-select', label: 'Home.page_ib.item5', icon: 'icon-transfer' },
  276. { path: '/pages/ib/agent-transfer', label: 'Home.page_ib.item9', icon: 'icon-transfer' },
  277. { path: '/pages/ib/recording', label: 'Home.page_ib.item7', icon: 'icon-application' },
  278. ],
  279. },
  280. {
  281. isOpenMenu: false,
  282. path: '/',
  283. label: 'Home.page_ib.item3',
  284. icon: 'crm-newspaper',
  285. children: [
  286. { path: '/pages/ib/report', label: 'Home.page_ib.item3', icon: 'icon-withdrawal' },
  287. { path: '/pages/ib/complexReport', label: 'Home.page_ib.item11', icon: 'icon-withdrawal' },
  288. ],
  289. },
  290. ])
  291. const menus = ref<MenuItem[]>([])
  292. // 监听 mode 变化,自动导航到对应首页
  293. watch(mode, (newMode, oldMode) => {
  294. if (newMode !== oldMode) {
  295. const base = newMode === 'customer' ? [...customerBaseMenus.value] : [...ibBaseMenus.value]
  296. if (shouldShowLanguageMenu.value) {
  297. base.push(languageMenuItem.value)
  298. }
  299. menus.value = cloneMenu(base)
  300. }
  301. }, { immediate: true })
  302. // 监听路由变化:自动展开包含当前路径的父菜单,不自动关闭其他菜单
  303. watch(route, () => {
  304. const currentPath = route.path
  305. const shouldOpenIndices: number[] = []
  306. menus.value.forEach((item, idx) => {
  307. if (item.children && item.children.length) {
  308. const isActive = item.children.some(child => {
  309. if (child.isExternal || child.type === 'lang') return false
  310. return currentPath === child.path || currentPath.startsWith(child.path + '?') || currentPath.startsWith(child.path + '/')
  311. })
  312. if (isActive && !item.isOpenMenu) {
  313. shouldOpenIndices.push(idx)
  314. }
  315. }
  316. })
  317. if (shouldOpenIndices.length) {
  318. shouldOpenIndices.forEach(idx => {
  319. menus.value[idx].isOpenMenu = true
  320. })
  321. nextTick(() => {
  322. shouldOpenIndices.forEach(idx => updateSubmenuHeight(idx))
  323. })
  324. }
  325. }, { immediate: true })
  326. watch(windowWidth, handleResize)
  327. onMounted(() => {
  328. nextTick(() => {
  329. menus.value.forEach((item, index) => {
  330. if (item.isOpenMenu && item.children && item.children.length) {
  331. updateSubmenuHeight(index)
  332. }
  333. })
  334. })
  335. })
  336. return {
  337. menus, // 最终菜单(已克隆,可直接修改 isOpenMenu 等)
  338. mode, // 只读或按需使用
  339. shouldShowLanguageMenu, // 可选,供外部获取状态
  340. windowWidth, // 可选,供外部获取宽度(300)
  341. setMode, // 可选,供外部设置模式
  342. setSubmenuRef, // 可选,供外部设置子菜单引用
  343. updateSubmenuHeight, // 可选,供外部更新子菜单高度
  344. handleClick, // 可选,供外部处理点击事件
  345. handleSubmenuClick, // 可选,供外部处理子菜单点击事件
  346. }
  347. }