index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <template>
  2. <div class="edit-wrap d-flex flex-column position-fixed">
  3. <!-- header -->
  4. <div class="edit-topbar position-relative d-flex justify-content-center align-items-center flex-grow-0 flex-shrink-0">
  5. <div class="mr-2">{{$t('dashboard.text_118')}}</div>
  6. <a-button size="small" type="primary" @click="handleConfirm" :loading="submiting">{{ $t('common.save') }}</a-button>
  7. <a-button size="small" @click="handleBack" class="ml-2">{{ $t('dialog.cancel') }}</a-button>
  8. <a-button size="small" @click="recovery" class="ml-2">{{ $t('dashboard.text_190') }}</a-button>
  9. </div>
  10. <!-- main -->
  11. <div class="edit-main position-relative flex-fill flex-nowrap align-items-stretch d-flex">
  12. <!-- extend gallery -->
  13. <extend-gallery
  14. ref="extend-gallery"
  15. class="extend-gallery position-relative" />
  16. <!-- edit main -->
  17. <main class="edit-content flex-fill position-relative">
  18. <div class="edit-content-inner w-100 h-100 position-absolute d-flex flex-column flex-nowrap">
  19. <div class="edit-header mb-2 d-flex">
  20. <a-input ref="input" v-model="dashboardName" :placeholder="$t('dashboard.text_119')" />
  21. <data-range v-if="(isAdminMode || isDomainMode) && $appConfig.isPrivate && !$store.getters.isSysCE" :dataRangeParams="dataRangeParams" @updateDataRange="updateDataRange" edit />
  22. </div>
  23. <grid-shadow
  24. class="flex-fill"
  25. ref="grid-shadow">
  26. <grid-layout
  27. ref="grid-layout"
  28. :layout.sync="layout"
  29. :col-num="colNum"
  30. :row-height="rowHeight"
  31. :max-rows="maxRows"
  32. :is-draggable="true"
  33. :is-resizable="true"
  34. :is-mirrored="false"
  35. :responsive="false"
  36. :margin="colMargin"
  37. :vertical-compact="false"
  38. :prevent-collision="true"
  39. :use-css-transforms="true">
  40. <template v-for="(item, index) in layout">
  41. <grid-item
  42. v-if="!['Quota', 'ProjectQuota'].includes(item.component) || (['Quota', 'ProjectQuota'].includes(item.component) && globalConfig.enable_quota_check)"
  43. class="edit-grid-item"
  44. :x="item.x"
  45. :y="item.y"
  46. :w="item.w"
  47. :h="item.h"
  48. :minH="item.minH"
  49. :i="item.i"
  50. :key="item.i"
  51. :is-draggable="!item.isTemplate"
  52. :is-resizable="!item.isTemplate"
  53. :style="{ outline: item.isTemplate ? '2px dashed darkmagenta' : '' }">
  54. <component :is="item.component" :chartId="`dashboard-item-${index}`" :options="item" :params="dashboardParams[item.i]" :dataRangeParams="dataRangeParams" @update="handleUpdateDashboardParams" edit>
  55. <template v-slot:actions="{ handleEdit }">
  56. <a-button class="p-0 h-auto" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleRemove(item)">
  57. <icon type="delete" />
  58. </a-button>
  59. <a-button class="p-0 h-auto ml-2" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleCopy(item, dashboardParams[item.i])">
  60. <icon type="copy" />
  61. </a-button>
  62. <a-button class="p-0 h-auto ml-2" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleEdit">
  63. <icon type="setting" />
  64. </a-button>
  65. </template>
  66. </component>
  67. </grid-item>
  68. </template>
  69. </grid-layout>
  70. </grid-shadow>
  71. </div>
  72. </main>
  73. </div>
  74. </div>
  75. </template>
  76. <script>
  77. import * as R from 'ramda'
  78. import { mapGetters } from 'vuex'
  79. import interact from '@interactjs/interactjs'
  80. import VueGridLayout from 'vue-grid-layout'
  81. import debounce from 'lodash/debounce'
  82. import getExtendsComponents from '@scope/extends'
  83. import GridShadow from '@Dashboard/components/GridShadow'
  84. import ExtendGallery from '@Dashboard/sections/ExtendGallery'
  85. import { clear as clearCache } from '@Dashboard/utils/cache'
  86. import { uuid } from '@/utils/utils'
  87. import storage from '@/utils/storage'
  88. import DataRange from '../dashboard/components/DataRange'
  89. const extendsComponents = R.is(Function, getExtendsComponents) ? getExtendsComponents() : getExtendsComponents
  90. export default {
  91. name: 'DashboardEdit',
  92. components: {
  93. GridLayout: VueGridLayout.GridLayout,
  94. GridItem: VueGridLayout.GridItem,
  95. GridShadow,
  96. ExtendGallery,
  97. DataRange,
  98. ...extendsComponents,
  99. },
  100. data () {
  101. return {
  102. submiting: false,
  103. dashboardName: '',
  104. dashboardParams: {},
  105. layout: [],
  106. layoutInit: [],
  107. colNum: 80,
  108. rowHeight: 30,
  109. colMargin: [15, 15],
  110. maxRows: 164,
  111. defaultGridW: 2,
  112. defaultGridH: 2,
  113. currentOption: null,
  114. dashboardOptions: [],
  115. isCheckSave: true,
  116. dataRangeParams: storage.get('__oc_dashboard_data_range__') || { scope: this.$store.getters.scope, domain: '', project: '' },
  117. }
  118. },
  119. computed: {
  120. ...mapGetters(['scope', 'globalConfig', 'isAdminMode', 'isDomainMode']),
  121. id () {
  122. return this.$route.query.id
  123. },
  124. isCreate () {
  125. return !this.id
  126. },
  127. },
  128. watch: {
  129. dashboardOptions (val) {
  130. if (!this.isCreate) {
  131. const item = R.find(R.propEq('id', this.$route.query.id))(val)
  132. if (item && item.name) {
  133. this.dashboardName = item.name
  134. } else {
  135. this.dashboardName = this.$t('dashboard.text_121')
  136. }
  137. }
  138. },
  139. },
  140. destroyed () {
  141. window.onbeforeunload = null
  142. this.pm = null
  143. this.debounceUpdateGridItem = null
  144. clearCache()
  145. },
  146. created () {
  147. this.pm = new this.$Manager('parameters', 'v1')
  148. this.fetchDashboardOptions()
  149. if (!this.isCreate) {
  150. this.fetchDashboard()
  151. }
  152. },
  153. beforeRouteLeave (to, from, next) {
  154. if (!this.isCheckSave) {
  155. next()
  156. } else {
  157. const answer = window.confirm(this.$t('dashboard.leave_page_tips'))
  158. if (answer) {
  159. next()
  160. } else {
  161. next(false)
  162. }
  163. }
  164. },
  165. mounted () {
  166. const tip = this.$t('dashboard.leave_page_tips')
  167. window.onbeforeunload = function (e) {
  168. e = e || window.event
  169. // 兼容IE8和Firefox 4之前的版本
  170. if (e) {
  171. e.returnValue = tip
  172. }
  173. // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
  174. return tip
  175. }
  176. this.extendGallery = this.$refs['extend-gallery']
  177. this.editMain = this.$refs['edit-main']
  178. this.dropzone = this.$refs['grid-shadow'].getContainerRef()
  179. this.dropzoneRect = this.dropzone.getBoundingClientRect()
  180. this.dropzoneY = this.dropzoneRect.y
  181. this.dropzoneX = this.dropzoneRect.x
  182. this.position = { x: 0, y: 0 }
  183. this.x = 0
  184. this.y = 0
  185. this.copy = null
  186. this.entered = false
  187. this.initItemInteract()
  188. this.initDropzoneInteract()
  189. this.debounceUpdateGridItem = debounce(this.updateGridItem, 500)
  190. },
  191. methods: {
  192. updateDataRange (params) {
  193. this.dataRangeParams = params
  194. },
  195. getActionStyle (component, params = {}) {
  196. if (component === 'Title' && params.color && params.color !== '#FFFFFF') {
  197. return { color: '#fff' }
  198. }
  199. return {}
  200. },
  201. recovery () {
  202. this.layout = R.clone(this.layoutInit)
  203. },
  204. async fetchDashboardOptions () {
  205. try {
  206. const response = await this.pm.get({ id: `dashboard_${this.scope}` })
  207. if (response.data && response.data.value) {
  208. this.dashboardOptions = response.data.value || []
  209. }
  210. // 如果是新建自动生成面板名称
  211. if (this.isCreate) {
  212. this.dashboardName = this.genDashboardName()
  213. }
  214. } catch (error) {
  215. if (error.response && error.response.status === 404) {
  216. this.pm.create({
  217. data: {
  218. name: `dashboard_${this.scope}`,
  219. value: [],
  220. },
  221. })
  222. }
  223. throw error
  224. }
  225. },
  226. async fetchDashboardWidgetParamter () {
  227. try {
  228. const response = await this.$store.dispatch('widgetSetting/getFetchWidgetSetting')
  229. if (response?.value && response.value[`dashboard-${this.scope}`]) {
  230. this.setData(response.value[`dashboard-${this.scope}`])
  231. }
  232. } catch (error) {
  233. console.log(error)
  234. }
  235. },
  236. async fetchDashboard () {
  237. try {
  238. const response = await this.pm.get({ id: this.id })
  239. if (response.data && response.data.value) {
  240. this.setData(response.data.value)
  241. }
  242. } catch (error) {
  243. await this.fetchDashboardWidgetParamter()
  244. throw error
  245. }
  246. },
  247. updateGridItem (_x, _y) {
  248. this.$refs['grid-layout'].eventBus.$emit(
  249. 'dragEvent',
  250. 'dragend',
  251. this.tempId,
  252. _x,
  253. _y,
  254. this.currentOption.w,
  255. this.currentOption.h,
  256. )
  257. },
  258. initItemInteract () {
  259. interact('.extend-gallery-item').draggable({
  260. inertia: true,
  261. listeners: {
  262. start: event => {
  263. event.target.parentNode.parentNode.classList.add('overflow-hidden')
  264. const component = event.target.dataset.component
  265. this.setCurrentOption(component)
  266. this.copy = event.target.cloneNode(true)
  267. this.copy.classList.add('drag')
  268. this.copy.style.top = `${event.target.offsetTop}px`
  269. event.target.parentNode.appendChild(this.copy)
  270. this.movingGridDeltaY = event.target.getBoundingClientRect().y
  271. this.tempId = `dashboard-item-${uuid(32)}`
  272. },
  273. move: event => {
  274. this.position.x += event.dx
  275. this.position.y += event.dy
  276. this.copy.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`
  277. this.copy.style.outline = '1px dashed darkmagenta'
  278. const editContainerScrollTop = document.querySelector('.grid-shadow-wrap').scrollTop
  279. const { x: _x, y: _y } = (this.calcXY(this.position.y + (this.movingGridDeltaY - 60 + editContainerScrollTop) - this.dropzoneY, this.position.x - this.dropzoneX))
  280. this.x = _x
  281. this.y = _y
  282. if (this.entered) {
  283. const currentDragGridData = this.layout[this.layout.length - 1]
  284. currentDragGridData.x = _x
  285. currentDragGridData.y = _y
  286. this.debounceUpdateGridItem(_x, _y)
  287. }
  288. },
  289. end: event => {
  290. event.target.parentNode.removeChild(this.copy)
  291. event.target.parentNode.parentNode.classList.remove('overflow-hidden')
  292. this.copy = null
  293. this.movingGridDeltaY = 0
  294. this.position = { x: 0, y: 0 }
  295. this.x = 0
  296. this.y = 0
  297. },
  298. },
  299. })
  300. },
  301. initDropzoneInteract () {
  302. interact(this.dropzone).dropzone({
  303. accept: '.extend-gallery-item',
  304. ondropactivate: event => {
  305. event.target.classList.add('drop-active')
  306. },
  307. ondragenter: event => {
  308. this.entered = true
  309. this.layout.push({
  310. x: 9999,
  311. y: this.y,
  312. w: this.currentOption.w,
  313. h: this.currentOption.h,
  314. minH: this.currentOption.minH,
  315. i: this.tempId,
  316. isTemplate: true,
  317. component: this.currentOption.component,
  318. })
  319. },
  320. ondragleave: () => {
  321. this.entered = false
  322. this.layout.splice(this.layout.length - 1, 1)
  323. },
  324. ondrop: () => {
  325. this.entered = false
  326. this.layout[this.layout.length - 1].isTemplate = false
  327. },
  328. ondropdeactivate: event => {
  329. event.target.classList.remove('drop-active')
  330. },
  331. })
  332. },
  333. calcXY (top, left) {
  334. const colWidth = this.calcColWidth()
  335. let x = Math.round((left - this.colMargin[0]) / (colWidth + this.colMargin[0]))
  336. let y = Math.round((top - this.colMargin[1]) / (this.rowHeight + this.colMargin[1]))
  337. // Capping
  338. x = Math.max(Math.min(x, this.colNum - this.defaultGridW), 0)
  339. y = Math.max(Math.min(y, this.maxRows - this.defaultGridH), 0)
  340. return { x, y }
  341. },
  342. calcColWidth () {
  343. const placeholderGrid = this.$refs['grid-layout'].$children[0]
  344. return (placeholderGrid.containerWidth - (this.colMargin[0] * (this.colNum + 1))) / this.colNum
  345. },
  346. setCurrentOption (component) {
  347. this.currentOption = {
  348. component,
  349. ...this.extendGallery.extendsOptions[component],
  350. }
  351. },
  352. handleRemove (item) {
  353. const index = R.findIndex(R.propEq('i', item.i))(this.layout)
  354. this.layout.splice(index, 1)
  355. },
  356. handleCopy (item, params) {
  357. const id = `dashboard-item-${uuid(32)}`
  358. this.dashboardParams[id] = params
  359. this.layout.push({
  360. component: item.component,
  361. h: item.h,
  362. i: id,
  363. w: item.w,
  364. x: item.x,
  365. y: item.y,
  366. })
  367. },
  368. handleUpdateDashboardParams (key, params) {
  369. this.$set(this.dashboardParams, key, params)
  370. },
  371. updateDashboardOptions (id) {
  372. return new Promise((resolve, reject) => {
  373. const options = [...this.dashboardOptions]
  374. const index = R.findIndex(R.propEq('id', id))(options)
  375. if (index !== -1) {
  376. options[index].name = this.dashboardName
  377. } else {
  378. options.push({ id, name: this.dashboardName })
  379. }
  380. this.pm.update({
  381. id: `dashboard_${this.scope}`,
  382. data: {
  383. value: options,
  384. },
  385. }).then(() => {
  386. resolve()
  387. }).catch(error => {
  388. reject(error)
  389. })
  390. })
  391. },
  392. createNewDashboard (id) {
  393. return new Promise((resolve, reject) => {
  394. this.updateDashboardOptions(id).then(() => {
  395. this.pm.create({
  396. data: {
  397. name: id,
  398. value: this.genData(),
  399. },
  400. }).then(response => {
  401. resolve(response)
  402. }).catch(error => {
  403. reject(error)
  404. })
  405. }).catch(error => {
  406. reject(error)
  407. })
  408. })
  409. },
  410. async updateDashboard (id) {
  411. return new Promise((resolve, reject) => {
  412. this.updateDashboardOptions(id).then(() => {
  413. this.pm.update({
  414. id,
  415. data: {
  416. value: this.genData(),
  417. },
  418. }).then(response => {
  419. resolve(response)
  420. }).catch(error => {
  421. reject(error)
  422. })
  423. }).catch(error => {
  424. reject(error)
  425. })
  426. })
  427. },
  428. async handleConfirm () {
  429. if (!R.trim(this.dashboardName)) {
  430. this.$message.warn(this.$t('dashboard.text_120'))
  431. this.$refs.input.focus()
  432. return
  433. }
  434. this.submting = true
  435. this.isCheckSave = false
  436. try {
  437. let id = this.id
  438. let response
  439. if (this.isCreate) {
  440. id = `dashboard-${this.scope}-panel-${uuid(16)}`
  441. response = await this.createNewDashboard(id)
  442. } else {
  443. response = await this.updateDashboard(id)
  444. }
  445. if (response) {
  446. storage.set(`__oc_dashboard_${this.scope}__`, { id: response.data.name, name: this.dashboardName })
  447. }
  448. this.$router.push('/dashboard')
  449. } finally {
  450. this.submting = false
  451. }
  452. },
  453. // 设置视图所需的data
  454. setData (data) {
  455. const dashboardParams = {}
  456. const layout = []
  457. R.forEachObjIndexed((value, key) => {
  458. dashboardParams[key] = value.params
  459. layout.push({
  460. ...value.layout,
  461. i: key,
  462. })
  463. }, data)
  464. this.dashboardParams = dashboardParams
  465. this.layoutInit = R.clone(layout)
  466. this.layout = layout
  467. },
  468. // 生成需要存储到配置中的data
  469. genData () {
  470. const ret = {}
  471. const layouts = this.layout.filter(item => this.dashboardParams[item.i])
  472. for (let i = 0, len = layouts.length; i < len; i++) {
  473. const layout = layouts[i]
  474. ret[layout.i] = {
  475. layout: {
  476. x: layout.x,
  477. y: layout.y,
  478. w: layout.w,
  479. h: layout.h,
  480. minH: layout.minH,
  481. component: layout.component,
  482. },
  483. params: this.dashboardParams[layout.i],
  484. }
  485. }
  486. return ret
  487. },
  488. handleBack () {
  489. this.$router.push('/')
  490. },
  491. // 根据现有的dashboard名称,生成新的dashboard名称(dashboard-1,dashboard-2,...)
  492. genDashboardName () {
  493. const reg = /^dashboard-\d+$/g
  494. const numbers = []
  495. let max = 1
  496. R.forEach(item => {
  497. if (R.test(reg, item.name)) {
  498. const nameArr = item.name.split('-')
  499. numbers.push(parseInt(nameArr[1]))
  500. }
  501. }, this.dashboardOptions)
  502. if (numbers.length > 0) {
  503. max = Math.max(...numbers)
  504. max += 1
  505. }
  506. return `dashboard-${max}`
  507. },
  508. },
  509. }
  510. </script>
  511. <style lang="less">
  512. @import url('../../styles/index.less');
  513. </style>
  514. <style lang="less" scoped>
  515. .edit-wrap {
  516. left: 0;
  517. right: 0;
  518. top: 0;
  519. bottom: 0;
  520. overflow: hidden;
  521. z-index: 0;
  522. }
  523. .edit-topbar {
  524. z-index: 5;
  525. box-shadow: 0 2px 4px 0 rgba(237, 237, 237, 0.5),
  526. 0 2px 4px 0 rgba(237, 237, 237, 0.5);
  527. height: 50px;
  528. }
  529. .edit-main {
  530. height: calc(100% - 40px);
  531. }
  532. .extend-gallery {
  533. z-index: 2;
  534. box-shadow: 4px 0px 5px 1px rgba(237, 237, 237, 0.5);
  535. }
  536. .edit-content {
  537. overflow: hidden;
  538. z-index: 1;
  539. }
  540. .edit-content-inner {
  541. top: 0;
  542. left: 0;
  543. z-index: 0;
  544. overflow: hidden;
  545. background-color: #f3f3f3;
  546. padding: 15px;
  547. }
  548. .edit-header {
  549. margin-left: 5px;
  550. }
  551. .edit-grid-item {
  552. user-select: none;
  553. background: rgba(0, 0, 0, 0.2);
  554. }
  555. ::v-deep .drop-active {
  556. border: 2px dashed skyblue;
  557. }
  558. ::v-deep .drop-active .vue-grid-item.vue-grid-placeholder {
  559. display: none;
  560. }
  561. </style>