You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

449 lines
12 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <template>
  2. <div class="productContent">
  3. <div class="selectBox" @click="handelMenu" :class="{ 'active': isOpen }">
  4. <span class="placeholder" :style="{ color: selectedItem ? '#333' : '#A8ABB2' }">
  5. {{ selectedItem || placeholder }}
  6. </span>
  7. <span class="arrow">
  8. <el-icon>
  9. <ArrowDown />
  10. </el-icon>
  11. </span>
  12. </div>
  13. <div class="menu" v-show="isOpen">
  14. <div class="coinselect" @click="coinhandelMenu" :class="{ 'active': coinisOpen }">
  15. <div class="cointxt">
  16. 金币产品
  17. </div>
  18. <span class="coin-arrow">
  19. <el-icon>
  20. <ArrowDown />
  21. </el-icon>
  22. </span>
  23. </div>
  24. <div class="coinoption" v-show="coinisOpen">
  25. <el-radio v-model="selectedValue" label="金币充值" size="large" />
  26. </div>
  27. <div class="product">
  28. <div class="coinselect" @click="producthandelMenu" :class="{ 'active': productisOpen }">
  29. <div class="cointxt">
  30. 软件产品
  31. </div>
  32. <span class="coin-arrow">
  33. <el-icon>
  34. <ArrowDown />
  35. </el-icon>
  36. </span>
  37. </div>
  38. <div class="productOption" v-show="productisOpen">
  39. <hr class="line">
  40. <div class="checktxt">软件</div>
  41. <div class="marketprodut">
  42. <div class="fistlevel" v-for="(menu, index) in menuData" :key="menu.name" @click="clickmenu(index)"
  43. :class="{ 'selected': menu.options.includes(selectedValue) }">
  44. <div class="label">
  45. {{ menu.name }}
  46. <el-icon :class="{ 'rotate': activeIndex === index }">
  47. <ArrowDown />
  48. </el-icon>
  49. </div>
  50. <div v-show="activeIndex == index" class="selectoption" @click.stop>
  51. <el-radio-group v-model="selectedValue">
  52. <div class="option" v-for="product in menu.options" :key="product">
  53. <el-radio :label="product">
  54. {{ product }}
  55. </el-radio>
  56. </div>
  57. </el-radio-group>
  58. </div>
  59. </div>
  60. </div>
  61. <hr class="line">
  62. <div class="ai">
  63. <div class="checktxt">AI机构探测神器</div>
  64. <hr class="line">
  65. <el-radio-group v-model="selectedValue">
  66. <el-radio v-for="ai in AIProduct" :key="ai" :label="ai" :value="ai">
  67. {{ ai }}
  68. </el-radio>
  69. </el-radio-group>
  70. </div>
  71. <div class="ai">
  72. <div class="checktxt">超级机构探测神器</div>
  73. <hr class="line">
  74. <el-radio-group v-model="selectedValue">
  75. <el-radio v-for="ai in superProduct" :key="ai" :label="ai" :value="ai">
  76. {{ ai }}
  77. </el-radio>
  78. </el-radio-group>
  79. </div>
  80. <div class="ai">
  81. <div class="checktxt">其他</div>
  82. <hr class="line">
  83. <el-radio-group v-model="selectedValue">
  84. <el-radio v-for="ai in InfoFee" :key="ai" :label="ai" :value="ai">
  85. {{ ai }}
  86. </el-radio>
  87. </el-radio-group>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. </template>
  94. <script setup>
  95. import { ref, watch, onMounted, computed, onUnmounted, nextTick } from 'vue';
  96. import { ArrowDown } from '@element-plus/icons-vue';
  97. const searchData = ref('')
  98. const isOpen = ref(false)
  99. const coinisOpen = ref(false)
  100. const productisOpen = ref(false)
  101. const selectedItem = ref('')
  102. const dropdownRef = ref(null)
  103. const placeholder = ref('请选择产品')
  104. const handelMenu = () => {
  105. isOpen.value = !isOpen.value
  106. ifselectAndOpen()
  107. }
  108. const coinhandelMenu = () => {
  109. coinisOpen.value = !coinisOpen.value
  110. }
  111. const producthandelMenu = () => {
  112. productisOpen.value = !productisOpen.value
  113. }
  114. // 接收父组件通过 v-model 传入的值
  115. const props = defineProps({
  116. modelValue: {
  117. type: String,
  118. default: ''
  119. }
  120. })
  121. const emit = defineEmits(['update:modelValue'])
  122. const selectedValue = ref('')
  123. watch(selectedValue, (newVal) => {
  124. emit('update:modelValue', newVal ? newVal : '');
  125. selectedItem.value = newVal || '';
  126. });
  127. const AIProduct = ['AI机构追踪', 'AI机构出击', 'AI机构资金', 'AI机活跃度','AI机构探测神器']
  128. const superProduct = ['超级机构透视', '超级机构伏击', '超级机构猎杀', '超级机构脉搏', '超级机构罗盘','超级机构探测神器']
  129. const InfoFee = ['静态信息费', '博股会员']
  130. const menuData = [
  131. {
  132. name: '美股',
  133. options: ['美股软件', '美股金卡', '美股套餐']
  134. },
  135. {
  136. name: '港股',
  137. options: ['港股软件', '港股金卡', '港股套餐']
  138. },
  139. {
  140. name: 'A股',
  141. options: ['A股软件', 'A股金卡', 'A股套餐']
  142. },
  143. {
  144. name: '新加坡股',
  145. options: ['新加坡股软件', '新加坡股金卡', '新加坡股套餐']
  146. },
  147. {
  148. name: '马股',
  149. options: ['马股软件', '马股金卡', '马股套餐']
  150. },
  151. {
  152. name: '日本股',
  153. options: ['日本股软件', '日本股金卡', '日本股套餐']
  154. },
  155. {
  156. name: '泰股',
  157. options: ['泰股软件', '泰股金卡', '泰股套餐']
  158. },
  159. {
  160. name: '越南股',
  161. options: ['越南股软件', '越南股金卡', '越南股套餐']
  162. },
  163. {
  164. name: '印尼股',
  165. options: ['印尼股软件', '印尼股金卡', '印尼股套餐']
  166. },
  167. {
  168. name: '韩国股',
  169. options: ['韩国股软件', '韩国股金卡', '韩国股套餐']
  170. },
  171. {
  172. name: '台湾股',
  173. options: ['台湾股软件', '台湾股金卡', '台湾股套餐']
  174. }
  175. ];
  176. //全局事件实现点击外部关闭选项
  177. const closeSoftwareSubmenu = () => {
  178. activeIndex.value = -1; // 将展开的二级菜单索引重置为 -1,实现关闭
  179. };
  180. const closeWholeDropdown = () => {
  181. isOpen.value = false; // 关闭整个弹窗
  182. // 可选:同时关闭弹窗内的子面板(如金币产品、软件产品展开面板)
  183. coinisOpen.value = false;
  184. productisOpen.value = false;
  185. closeSoftwareSubmenu(); // 同时关闭软件二级选项框
  186. };
  187. const handleGlobalClick = (e) => {
  188. // --- 原有:处理软件二级选项框外部关闭逻辑 ---
  189. if (productisOpen.value) {
  190. const submenuContainers = document.querySelectorAll('.selectoption');
  191. const firstLevelContainers = document.querySelectorAll('.fistlevel');
  192. let isClickInsideSubmenu = false;
  193. let isClickInsideFirstLevel = false;
  194. submenuContainers.forEach(container => container.contains(e.target) && (isClickInsideSubmenu = true));
  195. firstLevelContainers.forEach(container => container.contains(e.target) && (isClickInsideFirstLevel = true));
  196. if (!isClickInsideSubmenu && !isClickInsideFirstLevel) {
  197. closeSoftwareSubmenu();
  198. }
  199. }
  200. // --- 新增:处理整个弹窗外部关闭逻辑 ---
  201. if (isOpen.value) { // 仅当弹窗处于打开状态时判断
  202. // 获取整个下拉弹窗的 DOM 容器
  203. const dropdownContainer = document.querySelector('.menu');
  204. // 获取弹窗触发按钮(类名 .selectBox),点击按钮需正常切换弹窗,不触发关闭
  205. const triggerButton = document.querySelector('.selectBox');
  206. // 点击位置不在弹窗内,且不在触发按钮内 → 关闭整个弹窗
  207. if (!dropdownContainer?.contains(e.target) && !triggerButton?.contains(e.target)) {
  208. closeWholeDropdown();
  209. }
  210. }
  211. };
  212. const ifselectAndOpen = async () => {
  213. await nextTick();
  214. if (selectedValue.value == '金币充值') {
  215. coinisOpen.value = true
  216. } else if (selectedValue.value) {
  217. productisOpen.value = true
  218. } else {
  219. //不做处理
  220. }
  221. }
  222. //软件相关
  223. const activeIndex = ref(-1)
  224. const clickmenu = (index) => {
  225. activeIndex.value = activeIndex.value === index ? -1 : index;
  226. }
  227. const resetSelect = () => {
  228. selectedValue.value = ''; // 重置选中值
  229. selectedItem.value = ''; // 重置显示文本
  230. isOpen.value = false; // 关闭下拉菜单
  231. coinisOpen.value = false; // 关闭金币产品子菜单
  232. productisOpen.value = false; // 关闭软件产品子菜单
  233. activeIndex.value = -1; // 关闭二级菜单
  234. };
  235. watch(() => props.modelValue, (newVal) => {
  236. selectedItem.value = newVal;
  237. selectedValue.value = newVal;
  238. }, { immediate: true });
  239. onMounted(async () => {
  240. console.log('打开组件', props.modelValue);
  241. selectedValue.value = props.modelValue;
  242. selectedItem.value = selectedValue.value || '';
  243. document.addEventListener('click', handleGlobalClick); // 绑定全局事件
  244. });
  245. onUnmounted(() => {
  246. selectedValue.value = '';
  247. document.removeEventListener('click', handleGlobalClick); // 解绑全局事件
  248. });
  249. defineExpose({ resetSelect });
  250. </script>
  251. <style scoped lang="scss">
  252. .productContent {
  253. position: relative;
  254. width: 450px;
  255. font-family: 'Arial', sans-serif;
  256. }
  257. .selectBox {
  258. border: 1px solid #e5e7eb;
  259. padding: 4px 12px;
  260. height: 23px;
  261. cursor: pointer;
  262. display: flex;
  263. justify-content: space-between;
  264. align-items: center;
  265. background-color: #fff;
  266. border-radius: 6px;
  267. transition: all 0.3s ease;
  268. .placeholder {
  269. flex: 1;
  270. font-size: 14px;
  271. line-height: 18px;
  272. color: #A8ABB2;
  273. }
  274. }
  275. .arrow {
  276. margin-left: 8px;
  277. color: #999;
  278. transition: transform 0.3s ease;
  279. }
  280. .selectBox.active .arrow {
  281. transform: rotate(180deg);
  282. }
  283. .menu {
  284. position: absolute;
  285. top: 100%;
  286. left: 0;
  287. width: 160%;
  288. max-height: 700px;
  289. min-height: 200px;
  290. display: flex;
  291. padding: 10px;
  292. flex-direction: column;
  293. align-items: flex-start;
  294. gap: 10px;
  295. flex-shrink: 0;
  296. border-radius: 8px;
  297. background: #E4F0FC;
  298. box-shadow: 0 0 4px 0 #00000040;
  299. z-index: 100;
  300. .coinselect {
  301. width: 100px;
  302. height: 20px;
  303. border: 1px solid #175BE5;
  304. padding: 5px 0 5px 12px;
  305. display: flex;
  306. border-radius: 5px;
  307. .cointxt {
  308. width: 70px;
  309. height: 100%;
  310. display: flex;
  311. justify-content: center;
  312. align-items: center;
  313. color: #175be5;
  314. text-align: center;
  315. font-family: "PingFang SC";
  316. font-size: 14px;
  317. font-style: normal;
  318. font-weight: 700;
  319. line-height: 20px;
  320. }
  321. }
  322. .coin-arrow {
  323. flex: 1;
  324. display: flex;
  325. justify-content: center;
  326. align-items: center;
  327. color: #175BE5;
  328. }
  329. .coinselect.active .coin-arrow {
  330. transform: rotate(-90deg);
  331. }
  332. .product {
  333. width: 100%;
  334. .line {
  335. display: flex;
  336. height: 1px;
  337. padding: 0 16px;
  338. align-items: flex-start;
  339. align-content: flex-start;
  340. gap: 8px 60px;
  341. flex-shrink: 0;
  342. align-self: stretch;
  343. flex-wrap: wrap;
  344. border-top: 1px solid #7E91FF;
  345. }
  346. .checktxt {
  347. color: #5870ff;
  348. font-family: "PingFang SC";
  349. font-size: 13px;
  350. font-style: normal;
  351. font-weight: 700;
  352. line-height: 22px;
  353. margin: 10px 20px;
  354. }
  355. .productOption {
  356. width: 100%;
  357. .ai {
  358. width: 100%;
  359. }
  360. .marketprodut {
  361. width: 100%;
  362. display: flex;
  363. flex-wrap: wrap;
  364. gap: 10px;
  365. .fistlevel {
  366. position: relative;
  367. width: 130px;
  368. .label .el-icon {
  369. margin-left: 5px;
  370. transition: transform 0.3s ease;
  371. }
  372. .label .rotate {
  373. transform: rotate(-90deg); // 箭头旋转效果
  374. }
  375. .label {
  376. margin-left: 10px;
  377. }
  378. .selectoption {
  379. width: 100px;
  380. background-color: #fff;
  381. padding: 5px 20px;
  382. position: absolute;
  383. left: 60px;
  384. top: 0;
  385. z-index: 999;
  386. border: 1px solid #175BE5;
  387. border-radius: 6px;
  388. :deep(.el-checkbox__label) {
  389. color: #333333;
  390. font-family: "PingFang SC";
  391. font-size: 13px;
  392. font-style: normal;
  393. font-weight: 400;
  394. line-height: 22px;
  395. }
  396. }
  397. }
  398. .fistlevel.selected .label {
  399. color: #175BE5;
  400. }
  401. }
  402. }
  403. }
  404. }
  405. </style>