customIndicatorDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <script setup lang="ts">
  2. import type { BaseFieldItem, TableFields } from '@/types/Tables/table'
  3. import type { FormInstance, FormRules } from 'element-plus'
  4. import type {
  5. DialogProps,
  6. SaveForm,
  7. } from '@/types/Tables/customIndicatorDialog'
  8. import { reactive, ref, watch } from 'vue'
  9. import { useTable } from '@/hooks/useTable'
  10. import { useCustomIndicatorDialog } from '@/hooks/useCustomIndicatorDialog'
  11. import { useDialogDrag } from '@/hooks/useDialogDrag'
  12. import { Search } from '@element-plus/icons-vue'
  13. const { isFixedField } = useTable()
  14. const {
  15. initSortedTableFieldsInfo,
  16. validCustomName,
  17. updateFixedIndicator,
  18. updateUnfixedIndicator,
  19. getActivedIndicator,
  20. updateSortedTableFieldsInfo,
  21. } = useCustomIndicatorDialog()
  22. const props = withDefaults(defineProps<DialogProps>(), {
  23. SaveFormData: () => {
  24. return {
  25. schemeName: '默认',
  26. isSaveCustom: false,
  27. saveCustomName: '',
  28. }
  29. },
  30. })
  31. const emits = defineEmits(['updateFields'])
  32. const saveForm = reactive<SaveForm>(props.SaveFormData)
  33. const saveFormRules = reactive<FormRules<SaveForm>>({
  34. saveCustomName: [{ validator: validCustomName, trigger: 'blur' }],
  35. })
  36. // 保存自定义指标表单实例
  37. const saveFormRef = ref<FormInstance>()
  38. // 被排序过后的表格字段信息
  39. const sortedTableFieldsInfo = reactive<Array<TableFields>>([])
  40. // 点击应用后新生成的字段信息
  41. const newTableFieldsInfo = reactive<Array<TableFields>>([])
  42. // 自定义指标的展示状态
  43. const customIndicatorVisible = ref<boolean>(false)
  44. // 每次展开的时候,右侧栏目的排序需要跟表格字段的顺序同步
  45. // 不然会导致表格字段和右侧的栏目的顺序不一致
  46. // 关闭后需要重置这个状态,每次打开都要排一次
  47. const isFirstShow = ref<boolean>(true)
  48. // 自定义指标搜索值
  49. const indicatorSearch = ref<string>('')
  50. // 自定义指标左侧导航选中值
  51. const indicatorNavActive = ref<string>(props.defaultActiveNav)
  52. // 拖拽的容器
  53. const dragContentRef = ref<HTMLElement | null>(null)
  54. // 当前正在被拖拽的元素
  55. const dragingRef = ref<HTMLElement | null>(null)
  56. // 所有需要固定的指标
  57. const fixedIndicator = reactive<TableFields[]>([])
  58. // 所有需要不固定的指标
  59. const unFixedIndicator = reactive<TableFields[]>([])
  60. const { dragStart, dragEnter, dragOver, dragEnd } = useDialogDrag(
  61. dragingRef,
  62. dragContentRef,
  63. )
  64. // 当默认值变化的时候,要更新一下,这里主要用在初始化的时候,数据可能还没有获取到
  65. watch(
  66. () => props.defaultActiveNav,
  67. (newMenu: string) => {
  68. indicatorNavActive.value = newMenu
  69. },
  70. {
  71. deep: true,
  72. },
  73. )
  74. /**
  75. * @description: 多选框值改变,去更新不固定字段的展示
  76. * @return {*}
  77. */
  78. const changeIndicatorChecked = () => {
  79. updateUnfixedIndicator(
  80. props.indicatorFields,
  81. isFirstShow,
  82. props.tableFieldsInfo,
  83. unFixedIndicator,
  84. )
  85. }
  86. /**
  87. * @description: 填充表单的数据
  88. * @return {*}
  89. */
  90. const fillFomrData = () => {
  91. Object.assign(saveForm, props.SaveFormData)
  92. }
  93. /**
  94. * @description: 对话框打开
  95. * @return {*}
  96. */
  97. const indicatorOpen = () => {
  98. isFirstShow.value = true // 用于同步表格和右侧栏目的顺序
  99. console.log(props.SaveFormData)
  100. if (props.SaveFormData.schemeName !== '默认') {
  101. fillFomrData()
  102. }
  103. // 给不固定框一个初始值
  104. changeIndicatorChecked()
  105. updateFixedIndicator(props.indicatorFields, fixedIndicator) // 固定框给初始值
  106. }
  107. /**
  108. * @description: 对话框关闭
  109. * @return {*}
  110. */
  111. const indicatorClose = () => {
  112. // 重置到第一个锚点
  113. indicatorNavActive.value = props.indicatorFields[0]?.name
  114. // isSaveCustom.value = false
  115. saveForm.isSaveCustom = false
  116. }
  117. /**
  118. * @description: 跳转到指定的指标区域
  119. * @param {*} name 标签分段的字段名
  120. * @return {*}
  121. */
  122. const goToIndicator = (name: string) => {
  123. let el = document.querySelector(`div[data-anchor="${name}"]`) as HTMLElement
  124. if (!el) return
  125. indicatorNavActive.value = name
  126. el.scrollIntoView({ behavior: 'smooth' })
  127. }
  128. /**
  129. * @description: 展示自定义指标弹窗
  130. * @return {*}
  131. */
  132. const showCustomIndicator = () => {
  133. sortedTableFieldsInfo.splice(
  134. 0,
  135. sortedTableFieldsInfo.length,
  136. ...JSON.parse(JSON.stringify(props.tableFieldsInfo)),
  137. )
  138. customIndicatorVisible.value = true
  139. }
  140. /**
  141. * @description: 生成新的表格字段信息
  142. * @return {*}
  143. */
  144. const generateNewTableField = () => {
  145. customIndicatorVisible.value = false
  146. updateSortedTableFieldsInfo(
  147. sortedTableFieldsInfo,
  148. dragContentRef,
  149. unFixedIndicator,
  150. fixedIndicator,
  151. ) // 排序
  152. // 拿到当前激活的所有自定义指标
  153. let actived = getActivedIndicator(props.indicatorFields)
  154. // 把目前排好序的字段信息复制给新的字段信息
  155. newTableFieldsInfo.splice(
  156. 0,
  157. newTableFieldsInfo.length,
  158. ...JSON.parse(JSON.stringify(sortedTableFieldsInfo)),
  159. )
  160. // 根据当前已经激活的字段name,去吧字段信息中的state改为true
  161. newTableFieldsInfo.map(item => {
  162. if (actived.includes(item.name)) {
  163. item.state = true
  164. } else {
  165. item.state = false
  166. }
  167. })
  168. }
  169. /**
  170. * @description: 验证保存自定义指标的表单是否填写正确了
  171. * @param {FormInstance | undefined} saveFormRef 自定义指标表单实例
  172. * @return {Promise<boolean>} 验证结果
  173. */
  174. const validSaveForm = async (
  175. saveFormRef: FormInstance | undefined,
  176. ): Promise<boolean> => {
  177. if (!saveFormRef) return false
  178. let result = await saveFormRef.validate()
  179. return result
  180. }
  181. /**
  182. * @description: 应用配置的自定义指标
  183. * @return {*}
  184. */
  185. const applyCustomIndicator = async (saveFormRef: FormInstance | undefined) => {
  186. let fileName = null
  187. // 需要保存自定义指标为常用模板就需要去检查一下这个文件名
  188. if (saveForm.isSaveCustom) {
  189. let result = await validSaveForm(saveFormRef)
  190. if (!result) return
  191. fileName = saveForm.saveCustomName
  192. }
  193. generateNewTableField()
  194. emits('updateFields', newTableFieldsInfo, fileName)
  195. }
  196. /**
  197. * @description: 取消已经选中的指标
  198. * @param {*} name 字段名
  199. * @return {*}
  200. */
  201. const cancelSelect = (name: string) => {
  202. for (let i = 0; i < props.indicatorFields.length; i++) {
  203. let children = props.indicatorFields[i].children
  204. let result = children.find(item => item.name === name)
  205. console.log(props.indicatorFields[i])
  206. if (result) {
  207. result.state = false
  208. props.indicatorFields[i].value = props.indicatorFields[i].value.filter(
  209. item => item !== result.name,
  210. )
  211. changeIndicatorChecked()
  212. break
  213. }
  214. }
  215. }
  216. /**
  217. * @description: 判断当前是否有跟搜索框的值匹配的指标
  218. * @param {BaseFieldItem} field 当前指标组
  219. * @return {boolean} 是否有匹配的指标
  220. */
  221. const hasMatchIndicator = (field: BaseFieldItem<TableFields>): boolean => {
  222. let result = field.children.find(item => {
  223. return item.label.includes(indicatorSearch.value)
  224. })
  225. return Boolean(result)
  226. }
  227. initSortedTableFieldsInfo(sortedTableFieldsInfo, props.tableFieldsInfo)
  228. defineExpose({ showCustomIndicator })
  229. </script>
  230. <template>
  231. <div class="customIndicatorContainer">
  232. <el-dialog
  233. v-model="customIndicatorVisible"
  234. title="自定义指标"
  235. width="1096"
  236. :append-to-body="true"
  237. :align-center="true"
  238. @open="indicatorOpen"
  239. @close="indicatorClose"
  240. class="el-dialog"
  241. :close-on-click-modal="false"
  242. >
  243. <div class="indicatorBox">
  244. <div class="topWrapper">
  245. <el-input
  246. v-model="indicatorSearch"
  247. style="width: 240px"
  248. placeholder="输入指标名称搜索"
  249. :suffix-icon="Search"
  250. />
  251. </div>
  252. <div class="indicatorWrapper">
  253. <div class="indicatorSider">
  254. <div v-for="item in indicatorFields">
  255. <div
  256. v-show="hasMatchIndicator(item)"
  257. @click="goToIndicator(item.name)"
  258. class="indicatorCategory"
  259. :class="{
  260. indicatorCategoryActive: indicatorNavActive === item.name,
  261. }"
  262. >
  263. {{ item.label }}
  264. </div>
  265. </div>
  266. </div>
  267. <div class="indicatorContent">
  268. <div
  269. v-for="field in indicatorFields"
  270. :data-anchor="field.name"
  271. class="indicatorBlock"
  272. >
  273. <div v-show="hasMatchIndicator(field)">
  274. <div class="indicatorGroup">
  275. <span>{{ field.label }}</span>
  276. </div>
  277. <el-row class="indicatorItem">
  278. <template v-for="item in field.children">
  279. <el-col
  280. :span="8"
  281. v-show="item.label.includes(indicatorSearch)"
  282. >
  283. <el-checkbox-group
  284. v-model="field.value"
  285. @change="changeIndicatorChecked"
  286. >
  287. <el-checkbox
  288. style="margin-bottom: 16px"
  289. :value="item.name"
  290. :label="item.label"
  291. :disabled="isFixedField(props.fixedFields, item)"
  292. />
  293. </el-checkbox-group>
  294. </el-col>
  295. </template>
  296. </el-row>
  297. </div>
  298. </div>
  299. </div>
  300. </div>
  301. <div class="dragWrapper">
  302. <div class="dragHeader">
  303. <div class="dragTitle">已选指标</div>
  304. <div class="dragDes">拖拽可自定义指标顺序</div>
  305. <div class="fixedIndicator">
  306. <div class="dragBlock notAllow" v-for="item in fixedIndicator">
  307. {{ item.label }}
  308. </div>
  309. </div>
  310. <div class="dragSepreate">以上指标将横向固定</div>
  311. </div>
  312. <div
  313. class="dragContent"
  314. ref="dragContentRef"
  315. @dragstart="dragStart"
  316. @dragenter="dragEnter"
  317. @dragover="dragOver"
  318. @dragend="dragEnd"
  319. >
  320. <div
  321. draggable="true"
  322. v-for="item in unFixedIndicator"
  323. class="dragBlock"
  324. :name="item.name"
  325. >
  326. {{ item.label }}
  327. <el-icon @click="cancelSelect(item.name)" class="closeBtn"
  328. ><Close
  329. /></el-icon>
  330. </div>
  331. </div>
  332. </div>
  333. </div>
  334. <template #footer>
  335. <div class="dialogFooter">
  336. <div class="footerTools">
  337. <el-form
  338. ref="saveFormRef"
  339. :model="saveForm"
  340. :rules="saveFormRules"
  341. inline
  342. >
  343. <el-form-item class="saveFormItem" prop="isSaveCustom">
  344. <el-checkbox
  345. v-model="saveForm.isSaveCustom"
  346. label="保存为常用自定义指标"
  347. size="default"
  348. />
  349. </el-form-item>
  350. <el-form-item
  351. v-if="saveForm.isSaveCustom"
  352. prop="saveCustomName"
  353. class="saveFormItem"
  354. >
  355. <el-input
  356. v-model="saveForm.saveCustomName"
  357. style="width: 240px"
  358. placeholder="请输入自定义指标名"
  359. />
  360. </el-form-item>
  361. </el-form>
  362. </div>
  363. <div class="footerBtn">
  364. <el-button @click="customIndicatorVisible = false">取消</el-button>
  365. <el-button
  366. type="primary"
  367. @click="applyCustomIndicator(saveFormRef)"
  368. >
  369. 应用
  370. </el-button>
  371. </div>
  372. </div>
  373. </template>
  374. </el-dialog>
  375. </div>
  376. </template>
  377. <style lang="scss" scoped>
  378. .indicatorBox {
  379. position: relative;
  380. width: 100%;
  381. height: 100%;
  382. }
  383. .topWrapper {
  384. margin-bottom: 16px;
  385. }
  386. .indicatorWrapper {
  387. display: flex;
  388. width: 832px;
  389. height: 516px;
  390. border: 1px solid #eaebec;
  391. border-radius: 4px;
  392. }
  393. .indicatorSider {
  394. flex-shrink: 0;
  395. width: 160px;
  396. overflow: auto;
  397. border-right: 1px solid #eaebec;
  398. }
  399. .indicatorCategory {
  400. padding-left: 16px;
  401. font-size: 14px;
  402. line-height: 40px;
  403. color: #333;
  404. cursor: pointer;
  405. }
  406. .indicatorCategoryActive {
  407. color: #197afb;
  408. background-color: #d6eaff;
  409. }
  410. .indicatorContent {
  411. width: 672px;
  412. overflow: auto;
  413. scroll-behavior: smooth;
  414. }
  415. .indicatorGroup {
  416. color: rgb(51, 51, 51);
  417. color-scheme: light;
  418. display: block;
  419. font-weight: 700;
  420. font-size: 14px;
  421. word-break: break-all;
  422. -webkit-font-smoothing: subpixel-antialiased;
  423. margin-bottom: 16px;
  424. }
  425. .indicatorBlock {
  426. width: 100%;
  427. padding: 16px 0 0 24px;
  428. border-bottom: 1px solid #eaebec;
  429. // display: flex;
  430. // flex-wrap: wrap;
  431. }
  432. .dragWrapper {
  433. position: absolute;
  434. top: 0;
  435. right: 0;
  436. flex-shrink: 0;
  437. width: 216px;
  438. height: 565px;
  439. padding: 10px 0;
  440. overflow: auto;
  441. background-color: #f8f8f9;
  442. }
  443. .dragHeader,
  444. .dragContent {
  445. padding: 0 16px;
  446. }
  447. .dragTitle {
  448. font-size: 14px;
  449. font-weight: 700;
  450. line-height: 100%;
  451. color: #333;
  452. }
  453. .dragDes {
  454. margin: 8px 0;
  455. font-size: 12px;
  456. line-height: 100%;
  457. color: #999;
  458. }
  459. .dragBlock {
  460. position: relative;
  461. width: 184px;
  462. height: 40px;
  463. padding: 0 30px 0 36px;
  464. overflow: hidden;
  465. line-height: 40px;
  466. text-overflow: ellipsis;
  467. white-space: nowrap;
  468. background-color: #fff;
  469. border-bottom: 1px solid #e8eaec;
  470. cursor: all-scroll;
  471. transition: 0.1s all;
  472. }
  473. .dragBlock:hover {
  474. .closeBtn {
  475. display: block;
  476. }
  477. }
  478. .dragMove {
  479. background-color: white;
  480. border: 1px solid #197afb;
  481. }
  482. .closeBtn {
  483. display: none;
  484. cursor: pointer;
  485. position: absolute;
  486. top: 13px;
  487. right: 8px;
  488. }
  489. // 禁止拖拽
  490. .notAllow {
  491. cursor: not-allowed;
  492. }
  493. .dragSepreate {
  494. position: relative;
  495. margin: 16px 0 0;
  496. font-size: 12px;
  497. color: #999;
  498. text-align: center;
  499. }
  500. .dragWrapper .dragSepreate::before {
  501. position: absolute;
  502. top: 9px;
  503. left: 0;
  504. width: 32px;
  505. height: 1px;
  506. content: '';
  507. background-color: #e8eaec;
  508. }
  509. .dragWrapper .dragSepreate::after {
  510. position: absolute;
  511. top: 9px;
  512. right: 0;
  513. width: 32px;
  514. height: 1px;
  515. content: '';
  516. background-color: #e8eaec;
  517. }
  518. .dragWrapper .dragBlock::before {
  519. position: absolute;
  520. top: 16px;
  521. left: 12px;
  522. width: 16px;
  523. height: 2px;
  524. content: '';
  525. background-color: #999;
  526. }
  527. .dragWrapper .dragBlock::after {
  528. position: absolute;
  529. bottom: 16px;
  530. left: 12px;
  531. width: 16px;
  532. height: 2px;
  533. content: '';
  534. background-color: #999;
  535. }
  536. .dragContent {
  537. margin-top: 16px;
  538. overflow-x: hidden;
  539. overflow-y: auto;
  540. }
  541. .saveFormItem {
  542. margin-bottom: 0;
  543. margin-right: 15px;
  544. // padding: 0 5px;
  545. }
  546. .dialogFooter {
  547. display: flex;
  548. align-items: center;
  549. justify-content: space-between;
  550. }
  551. </style>