useMenuSplit.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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. switch (code) {
  48. case 'follow':
  49. router.reLaunch('/pages/follow/index')
  50. break
  51. case 'ib':
  52. router.reLaunch('/pages/ib/index')
  53. break
  54. case 'customer':
  55. router.reLaunch('/pages/customer/index')
  56. break
  57. default:
  58. break
  59. }
  60. if (code === 'ib') {
  61. uni.$emit('open-ib')
  62. }
  63. nextTick(() => {
  64. submenuRefs.value = [] // 重置,等待重新绑定
  65. menus.value.forEach((item, index) => {
  66. if (item.isOpenMenu && item.children && item.children.length) {
  67. updateSubmenuHeight(index)
  68. }
  69. })
  70. })
  71. }
  72. // 测量元素实际高度(用于过渡动画)
  73. const measureHeight = (element: HTMLElement): number => {
  74. const originalDisplay = element.style.display
  75. const originalPosition = element.style.position
  76. const originalVisibility = element.style.visibility
  77. const originalWidth = element.style.width
  78. element.style.display = 'block'
  79. element.style.position = 'absolute'
  80. element.style.visibility = 'hidden'
  81. element.style.width = '100%'
  82. const height = element.scrollHeight || element.offsetHeight
  83. element.style.display = originalDisplay
  84. element.style.position = originalPosition
  85. element.style.visibility = originalVisibility
  86. element.style.width = originalWidth
  87. return height
  88. }
  89. // 更新指定索引的子菜单高度
  90. const updateSubmenuHeight = (index: number) => {
  91. const refs = submenuRefs.value
  92. nextTick(() => {
  93. if (refs && refs[index]) {
  94. const el = refs[index].$el || refs[index]
  95. const height = measureHeight(el)
  96. if (height > 0) {
  97. menus.value[index].submenuHeight = height
  98. }
  99. }
  100. })
  101. }
  102. let clickTimer: ReturnType<typeof setTimeout> | null = null
  103. // 点击菜单项(切换展开/折叠或跳转)
  104. function handleClick(index: number) {
  105. if (clickTimer) return
  106. clickTimer = setTimeout(() => {
  107. clickTimer = null
  108. }, 300)
  109. const item = menus.value[index]
  110. // 无子菜单:执行跳转或特殊操作
  111. if (!item.children || item.children.length === 0) {
  112. // #ifdef H5
  113. if (item.type === 'chat' && !shouldShowLanguageMenu.value) {
  114. if (LiveChatService) {
  115. LiveChatService.showChat();
  116. }
  117. return
  118. } else {
  119. shouldShowLanguageMenu.value && handleClick1(item)
  120. router.push(item.path)
  121. return
  122. }
  123. // #endif
  124. shouldShowLanguageMenu.value && handleClick1(item)
  125. router.push(item.path)
  126. return
  127. }
  128. // 有子菜单:切换展开/折叠状态
  129. item.isOpenMenu = !item.isOpenMenu
  130. if (item.isOpenMenu) {
  131. nextTick(() => updateSubmenuHeight(index))
  132. }
  133. }
  134. // 子菜单项点击事件(由 cwg-submenu 组件发出)
  135. function handleSubmenuClick(subItem: any) {
  136. // 处理语言切换
  137. if (subItem.type === 'lang') {
  138. locale.value = subItem.lang
  139. return
  140. }
  141. // 处理外部链接
  142. if (subItem.isExternal) {
  143. // #ifdef H5
  144. window.open(subItem.path, '_blank')
  145. // #endif
  146. // #ifdef APP-PLUS
  147. plus.runtime.openURL(subItem.path)
  148. // #endif
  149. return
  150. }
  151. // 内部页面跳转
  152. shouldShowLanguageMenu.value && handleClick1(subItem)
  153. router.push(subItem.path)
  154. }
  155. // 窗口大小变化时重新计算所有已展开子菜单的高度
  156. const handleResize = () => {
  157. menus.value.forEach((item, index) => {
  158. if (item.isOpenMenu && item.children && item.children.length) {
  159. updateSubmenuHeight(index)
  160. }
  161. })
  162. }
  163. const customMenuList = computed(() =>
  164. localesList.map((code) => ({
  165. label: `language.${code}`,
  166. lang: code,
  167. type: "lang",
  168. path: '/'
  169. }))
  170. )
  171. const languageMenuItem = computed<MenuItem>(() => ({
  172. path: '/',
  173. isOpenMenu: false,
  174. label: 'language.index',
  175. icon: 'cwg-lang',
  176. children: customMenuList.value,
  177. submenuHeight: 0,
  178. }))
  179. const customerBaseMenus = computed<MenuItem[]>(() => [
  180. {
  181. isOpenMenu: false,
  182. submenuHeight: 0,
  183. path: '/',
  184. label: 'Shop.Index.Transaction',
  185. icon: 'crm-trade',
  186. children: [
  187. { path: '/pages/customer/index', label: 'Custom.Index.AccountList', icon: 'icon-client' },
  188. { path: '/pages/customer/trade-history', label: 'Ib.Report.Tit1', icon: 'icon-transfer' },
  189. { path: '/pages/customer/trade-position', label: 'Ib.Report.Tit4', icon: 'icon-transfer' },
  190. { path: '/pages/customer/recording-history', label: 'Home.page_customer.item7', icon: 'icon-application' },
  191. ],
  192. },
  193. {
  194. isOpenMenu: false,
  195. submenuHeight: 0,
  196. path: '/',
  197. label: 'vu.item6',
  198. icon: 'crm-payment',
  199. children: [
  200. { path: '/pages/customer/deposit-select', label: 'Home.page_customer.item2', icon: 'icon-deposit' },
  201. { path: '/pages/customer/withdrawal-select', label: 'Home.page_customer.item3', icon: 'icon-withdrawal' },
  202. { path: '/pages/customer/payment-history', label: 'Home.page_customer.item4', icon: 'icon-payment' },
  203. { path: '/pages/customer/transfer', label: 'Custom.Index.Transfer', icon: 'icon-transfer' },
  204. { path: '/pages/customer/wallet-transfer', label: 'wallet.item62', icon: 'icon-transfer' },
  205. { path: '/pages/customer/wallet-history', label: 'wallet.item7', icon: 'icon-transfer' },
  206. ],
  207. },
  208. {
  209. path: '/pages/activities/index',
  210. isOpenMenu: false,
  211. label: 'Home.page_customer.item6',
  212. icon: 'crm-hd',
  213. children: [],
  214. },
  215. {
  216. path: '/',
  217. isOpenMenu: false,
  218. label: 'vu.item5',
  219. icon: 'crm-chart-area',
  220. children: [
  221. { path: '/pages/analytics/analystViews', label: 'News.Announcement', icon: 'icon-application' },
  222. { path: '/pages/analytics/news', label: 'News.NewsInformation', icon: 'icon-application' },
  223. // {
  224. // path: `https://www.${host}.com/${locale.value}/economic-calendar`,
  225. // label: 'News.FinancialCalendar',
  226. // icon: 'icon-application',
  227. // isExternal: true,
  228. // },
  229. ],
  230. },
  231. {
  232. path: '/pages/common/download',
  233. isOpenMenu: false,
  234. label: 'Downloadpage.item1',
  235. icon: 'crm-download',
  236. children: [],
  237. },
  238. {
  239. path: '/pages/common/chat',
  240. isOpenMenu: false,
  241. label: 'Downloadpage.item16',
  242. icon: 'crm-headset',
  243. children: [],
  244. type: 'chat',
  245. },
  246. {
  247. path: '/',
  248. isOpenMenu: false,
  249. label: 'Custom.Index.Settings',
  250. icon: 'crm-sz',
  251. children: [
  252. { path: '/pages/mine/info?type=1', label: 'PersonalManagement.Title.PersonalInformation', icon: 'crm-headset' },
  253. { path: '/pages/mine/info?type=2', label: 'PersonalManagement.Title.BankInformation', icon: 'crm-headset' },
  254. { path: '/pages/mine/info?type=3', label: 'PersonalManagement.Title.FileManagement', icon: 'crm-headset' },
  255. { path: '/pages/mine/info?type=4', label: 'PersonalManagement.Title.SecurityCenter', icon: 'crm-headset' },
  256. { path: '/pages/common/notice', label: 'News.Notice', icon: 'crm-headset' },
  257. ],
  258. },
  259. ])
  260. const ibBaseMenus = computed<MenuItem[]>(() => [
  261. {
  262. isOpenMenu: false,
  263. path: '/pages/ib/index',
  264. label: 'Documentary.console.item1',
  265. icon: 'crm-mb',
  266. },
  267. {
  268. path: '/',
  269. label: 'Ib.Custom.Manage3',
  270. icon: 'crm-bg',
  271. children: [
  272. { path: '/pages/ib/customer', label: 'Ib.Custom.Manage3', icon: 'icon-deposit' },
  273. { path: '/pages/ib/subsList', label: 'Ib.Custom.Manage2', icon: 'icon-deposit' },
  274. { path: '/pages/ib/agentList', label: 'Documentary.console.item23', icon: 'icon-deposit' },
  275. { path: '/pages/ib/accountList', label: 'Ib.Custom.Manage1', icon: 'icon-deposit' }
  276. ],
  277. },
  278. {
  279. isOpenMenu: false,
  280. submenuHeight: 0,
  281. path: '/',
  282. label: 'vu.item6',
  283. icon: 'crm-payment',
  284. children: [
  285. { path: '/pages/ib/transfer', label: 'Home.page_ib.item4', icon: 'icon-payment' },
  286. { path: '/pages/ib/withdraw-select', label: 'Home.page_ib.item5', icon: 'icon-transfer' },
  287. { path: '/pages/ib/agent-transfer', label: 'Home.page_ib.item9', icon: 'icon-transfer' },
  288. { path: '/pages/ib/recording', label: 'Home.page_ib.item7', icon: 'icon-application' },
  289. ],
  290. },
  291. {
  292. isOpenMenu: false,
  293. path: '/',
  294. label: 'Home.page_ib.item3',
  295. icon: 'crm-newspaper',
  296. children: [
  297. { path: '/pages/ib/report', label: 'Home.page_ib.item3', icon: 'icon-withdrawal' },
  298. { path: '/pages/ib/complexReport', label: 'Home.page_ib.item11', icon: 'icon-withdrawal' },
  299. ],
  300. },
  301. ])
  302. const followBaseMenus = computed<MenuItem[]>(() => [
  303. {
  304. isOpenMenu: false,
  305. path: '/pages/follow/index',
  306. label: 'Documentary.console.item1',
  307. icon: 'crm-mb',
  308. },
  309. {
  310. isOpenMenu: false,
  311. path: '/pages/follow/trading-center',
  312. label: 'Documentary.page_doc.item2',
  313. icon: 'crm-gd',
  314. },
  315. {
  316. isOpenMenu: false,
  317. submenuHeight: 0,
  318. path: '/',
  319. label: 'Documentary.page_doc.item3',
  320. icon: 'crm-newspaper',
  321. children: [
  322. { path: '/pages/follow/report', label: 'Documentary.page_doc.item3', icon: 'icon-client' },
  323. ],
  324. },
  325. {
  326. isOpenMenu: false,
  327. submenuHeight: 0,
  328. path: '/',
  329. label: 'Documentary.page_doc.item4',
  330. icon: 'crm-payment',
  331. children: [
  332. { path: '/pages/follow/transfer', label: 'Documentary.TundManagement.item2', icon: 'icon-client' },
  333. { path: '/pages/follow/transfer-history', label: 'Documentary.TundManagement.item3', icon: 'icon-transfer' }
  334. ],
  335. },
  336. {
  337. isOpenMenu: false,
  338. submenuHeight: 0,
  339. path: '/',
  340. label: 'Documentary.page_doc.item5',
  341. icon: 'crm-trade',
  342. children: [
  343. { path: '/pages/follow/trading-management', label: 'Documentary.TundManagement.item8', icon: 'icon-client' },
  344. { path: '/pages/follow/account-management', label: 'Documentary.TundManagement.item9', icon: 'icon-transfer' },
  345. { path: '/pages/follow/record', label: 'Documentary.TundManagement.item10', icon: 'icon-transfer' }
  346. ],
  347. },
  348. ])
  349. const menus = ref<MenuItem[]>([])
  350. // 监听 mode 变化,自动导航到对应首页
  351. watch(mode, (newMode, oldMode) => {
  352. if (newMode !== oldMode) {
  353. let base: MenuItem[] = []
  354. switch (newMode) {
  355. case 'follow':
  356. base = [...followBaseMenus.value]
  357. break
  358. case 'ib':
  359. base = [...ibBaseMenus.value]
  360. break
  361. case 'customer':
  362. base = [...customerBaseMenus.value]
  363. break
  364. default:
  365. break
  366. }
  367. if (shouldShowLanguageMenu.value) {
  368. base.push(languageMenuItem.value)
  369. }
  370. menus.value = cloneMenu(base)
  371. }
  372. }, { immediate: true })
  373. // 监听路由变化:自动展开包含当前路径的父菜单,不自动关闭其他菜单
  374. watch(route, () => {
  375. const currentPath = route.path
  376. const shouldOpenIndices: number[] = []
  377. menus.value.forEach((item, idx) => {
  378. if (item.children && item.children.length) {
  379. const isActive = item.children.some(child => {
  380. if (child.isExternal || child.type === 'lang') return false
  381. return currentPath === child.path || currentPath.startsWith(child.path + '?') || currentPath.startsWith(child.path + '/')
  382. })
  383. if (isActive && !item.isOpenMenu) {
  384. shouldOpenIndices.push(idx)
  385. }
  386. }
  387. })
  388. if (shouldOpenIndices.length) {
  389. shouldOpenIndices.forEach(idx => {
  390. menus.value[idx].isOpenMenu = true
  391. })
  392. nextTick(() => {
  393. shouldOpenIndices.forEach(idx => updateSubmenuHeight(idx))
  394. })
  395. }
  396. }, { immediate: true })
  397. watch(windowWidth, handleResize)
  398. onMounted(() => {
  399. nextTick(() => {
  400. menus.value.forEach((item, index) => {
  401. if (item.isOpenMenu && item.children && item.children.length) {
  402. updateSubmenuHeight(index)
  403. }
  404. })
  405. })
  406. })
  407. return {
  408. menus, // 最终菜单(已克隆,可直接修改 isOpenMenu 等)
  409. mode, // 只读或按需使用
  410. shouldShowLanguageMenu, // 可选,供外部获取状态
  411. windowWidth, // 可选,供外部获取宽度(300)
  412. setMode, // 可选,供外部设置模式
  413. setSubmenuRef, // 可选,供外部设置子菜单引用
  414. updateSubmenuHeight, // 可选,供外部更新子菜单高度
  415. handleClick, // 可选,供外部处理点击事件
  416. handleSubmenuClick, // 可选,供外部处理子菜单点击事件
  417. }
  418. }