useMenuSplit.ts 13 KB

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