| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- <template>
- <div class="edit-wrap d-flex flex-column position-fixed">
- <!-- header -->
- <div class="edit-topbar position-relative d-flex justify-content-center align-items-center flex-grow-0 flex-shrink-0">
- <div class="mr-2">{{$t('dashboard.text_118')}}</div>
- <a-button size="small" type="primary" @click="handleConfirm" :loading="submiting">{{ $t('common.save') }}</a-button>
- <a-button size="small" @click="handleBack" class="ml-2">{{ $t('dialog.cancel') }}</a-button>
- <a-button size="small" @click="recovery" class="ml-2">{{ $t('dashboard.text_190') }}</a-button>
- </div>
- <!-- main -->
- <div class="edit-main position-relative flex-fill flex-nowrap align-items-stretch d-flex">
- <!-- extend gallery -->
- <extend-gallery
- ref="extend-gallery"
- class="extend-gallery position-relative" />
- <!-- edit main -->
- <main class="edit-content flex-fill position-relative">
- <div class="edit-content-inner w-100 h-100 position-absolute d-flex flex-column flex-nowrap">
- <div class="edit-header mb-2 d-flex">
- <a-input ref="input" v-model="dashboardName" :placeholder="$t('dashboard.text_119')" />
- <data-range v-if="(isAdminMode || isDomainMode) && $appConfig.isPrivate && !$store.getters.isSysCE" :dataRangeParams="dataRangeParams" @updateDataRange="updateDataRange" edit />
- </div>
- <grid-shadow
- class="flex-fill"
- ref="grid-shadow">
- <grid-layout
- ref="grid-layout"
- :layout.sync="layout"
- :col-num="colNum"
- :row-height="rowHeight"
- :max-rows="maxRows"
- :is-draggable="true"
- :is-resizable="true"
- :is-mirrored="false"
- :responsive="false"
- :margin="colMargin"
- :vertical-compact="false"
- :prevent-collision="true"
- :use-css-transforms="true">
- <template v-for="(item, index) in layout">
- <grid-item
- v-if="!['Quota', 'ProjectQuota'].includes(item.component) || (['Quota', 'ProjectQuota'].includes(item.component) && globalConfig.enable_quota_check)"
- class="edit-grid-item"
- :x="item.x"
- :y="item.y"
- :w="item.w"
- :h="item.h"
- :minH="item.minH"
- :i="item.i"
- :key="item.i"
- :is-draggable="!item.isTemplate"
- :is-resizable="!item.isTemplate"
- :style="{ outline: item.isTemplate ? '2px dashed darkmagenta' : '' }">
- <component :is="item.component" :chartId="`dashboard-item-${index}`" :options="item" :params="dashboardParams[item.i]" :dataRangeParams="dataRangeParams" @update="handleUpdateDashboardParams" edit>
- <template v-slot:actions="{ handleEdit }">
- <a-button class="p-0 h-auto" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleRemove(item)">
- <icon type="delete" />
- </a-button>
- <a-button class="p-0 h-auto ml-2" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleCopy(item, dashboardParams[item.i])">
- <icon type="copy" />
- </a-button>
- <a-button class="p-0 h-auto ml-2" type="link" :style="getActionStyle(item.component, dashboardParams[item.i])" @click="handleEdit">
- <icon type="setting" />
- </a-button>
- </template>
- </component>
- </grid-item>
- </template>
- </grid-layout>
- </grid-shadow>
- </div>
- </main>
- </div>
- </div>
- </template>
- <script>
- import * as R from 'ramda'
- import { mapGetters } from 'vuex'
- import interact from '@interactjs/interactjs'
- import VueGridLayout from 'vue-grid-layout'
- import debounce from 'lodash/debounce'
- import getExtendsComponents from '@scope/extends'
- import GridShadow from '@Dashboard/components/GridShadow'
- import ExtendGallery from '@Dashboard/sections/ExtendGallery'
- import { clear as clearCache } from '@Dashboard/utils/cache'
- import { uuid } from '@/utils/utils'
- import storage from '@/utils/storage'
- import DataRange from '../dashboard/components/DataRange'
- const extendsComponents = R.is(Function, getExtendsComponents) ? getExtendsComponents() : getExtendsComponents
- export default {
- name: 'DashboardEdit',
- components: {
- GridLayout: VueGridLayout.GridLayout,
- GridItem: VueGridLayout.GridItem,
- GridShadow,
- ExtendGallery,
- DataRange,
- ...extendsComponents,
- },
- data () {
- return {
- submiting: false,
- dashboardName: '',
- dashboardParams: {},
- layout: [],
- layoutInit: [],
- colNum: 80,
- rowHeight: 30,
- colMargin: [15, 15],
- maxRows: 164,
- defaultGridW: 2,
- defaultGridH: 2,
- currentOption: null,
- dashboardOptions: [],
- isCheckSave: true,
- dataRangeParams: storage.get('__oc_dashboard_data_range__') || { scope: this.$store.getters.scope, domain: '', project: '' },
- }
- },
- computed: {
- ...mapGetters(['scope', 'globalConfig', 'isAdminMode', 'isDomainMode']),
- id () {
- return this.$route.query.id
- },
- isCreate () {
- return !this.id
- },
- },
- watch: {
- dashboardOptions (val) {
- if (!this.isCreate) {
- const item = R.find(R.propEq('id', this.$route.query.id))(val)
- if (item && item.name) {
- this.dashboardName = item.name
- } else {
- this.dashboardName = this.$t('dashboard.text_121')
- }
- }
- },
- },
- destroyed () {
- window.onbeforeunload = null
- this.pm = null
- this.debounceUpdateGridItem = null
- clearCache()
- },
- created () {
- this.pm = new this.$Manager('parameters', 'v1')
- this.fetchDashboardOptions()
- if (!this.isCreate) {
- this.fetchDashboard()
- }
- },
- beforeRouteLeave (to, from, next) {
- if (!this.isCheckSave) {
- next()
- } else {
- const answer = window.confirm(this.$t('dashboard.leave_page_tips'))
- if (answer) {
- next()
- } else {
- next(false)
- }
- }
- },
- mounted () {
- const tip = this.$t('dashboard.leave_page_tips')
- window.onbeforeunload = function (e) {
- e = e || window.event
- // 兼容IE8和Firefox 4之前的版本
- if (e) {
- e.returnValue = tip
- }
- // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
- return tip
- }
- this.extendGallery = this.$refs['extend-gallery']
- this.editMain = this.$refs['edit-main']
- this.dropzone = this.$refs['grid-shadow'].getContainerRef()
- this.dropzoneRect = this.dropzone.getBoundingClientRect()
- this.dropzoneY = this.dropzoneRect.y
- this.dropzoneX = this.dropzoneRect.x
- this.position = { x: 0, y: 0 }
- this.x = 0
- this.y = 0
- this.copy = null
- this.entered = false
- this.initItemInteract()
- this.initDropzoneInteract()
- this.debounceUpdateGridItem = debounce(this.updateGridItem, 500)
- },
- methods: {
- updateDataRange (params) {
- this.dataRangeParams = params
- },
- getActionStyle (component, params = {}) {
- if (component === 'Title' && params.color && params.color !== '#FFFFFF') {
- return { color: '#fff' }
- }
- return {}
- },
- recovery () {
- this.layout = R.clone(this.layoutInit)
- },
- async fetchDashboardOptions () {
- try {
- const response = await this.pm.get({ id: `dashboard_${this.scope}` })
- if (response.data && response.data.value) {
- this.dashboardOptions = response.data.value || []
- }
- // 如果是新建自动生成面板名称
- if (this.isCreate) {
- this.dashboardName = this.genDashboardName()
- }
- } catch (error) {
- if (error.response && error.response.status === 404) {
- this.pm.create({
- data: {
- name: `dashboard_${this.scope}`,
- value: [],
- },
- })
- }
- throw error
- }
- },
- async fetchDashboardWidgetParamter () {
- try {
- const response = await this.$store.dispatch('widgetSetting/getFetchWidgetSetting')
- if (response?.value && response.value[`dashboard-${this.scope}`]) {
- this.setData(response.value[`dashboard-${this.scope}`])
- }
- } catch (error) {
- console.log(error)
- }
- },
- async fetchDashboard () {
- try {
- const response = await this.pm.get({ id: this.id })
- if (response.data && response.data.value) {
- this.setData(response.data.value)
- }
- } catch (error) {
- await this.fetchDashboardWidgetParamter()
- throw error
- }
- },
- updateGridItem (_x, _y) {
- this.$refs['grid-layout'].eventBus.$emit(
- 'dragEvent',
- 'dragend',
- this.tempId,
- _x,
- _y,
- this.currentOption.w,
- this.currentOption.h,
- )
- },
- initItemInteract () {
- interact('.extend-gallery-item').draggable({
- inertia: true,
- listeners: {
- start: event => {
- event.target.parentNode.parentNode.classList.add('overflow-hidden')
- const component = event.target.dataset.component
- this.setCurrentOption(component)
- this.copy = event.target.cloneNode(true)
- this.copy.classList.add('drag')
- this.copy.style.top = `${event.target.offsetTop}px`
- event.target.parentNode.appendChild(this.copy)
- this.movingGridDeltaY = event.target.getBoundingClientRect().y
- this.tempId = `dashboard-item-${uuid(32)}`
- },
- move: event => {
- this.position.x += event.dx
- this.position.y += event.dy
- this.copy.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`
- this.copy.style.outline = '1px dashed darkmagenta'
- const editContainerScrollTop = document.querySelector('.grid-shadow-wrap').scrollTop
- const { x: _x, y: _y } = (this.calcXY(this.position.y + (this.movingGridDeltaY - 60 + editContainerScrollTop) - this.dropzoneY, this.position.x - this.dropzoneX))
- this.x = _x
- this.y = _y
- if (this.entered) {
- const currentDragGridData = this.layout[this.layout.length - 1]
- currentDragGridData.x = _x
- currentDragGridData.y = _y
- this.debounceUpdateGridItem(_x, _y)
- }
- },
- end: event => {
- event.target.parentNode.removeChild(this.copy)
- event.target.parentNode.parentNode.classList.remove('overflow-hidden')
- this.copy = null
- this.movingGridDeltaY = 0
- this.position = { x: 0, y: 0 }
- this.x = 0
- this.y = 0
- },
- },
- })
- },
- initDropzoneInteract () {
- interact(this.dropzone).dropzone({
- accept: '.extend-gallery-item',
- ondropactivate: event => {
- event.target.classList.add('drop-active')
- },
- ondragenter: event => {
- this.entered = true
- this.layout.push({
- x: 9999,
- y: this.y,
- w: this.currentOption.w,
- h: this.currentOption.h,
- minH: this.currentOption.minH,
- i: this.tempId,
- isTemplate: true,
- component: this.currentOption.component,
- })
- },
- ondragleave: () => {
- this.entered = false
- this.layout.splice(this.layout.length - 1, 1)
- },
- ondrop: () => {
- this.entered = false
- this.layout[this.layout.length - 1].isTemplate = false
- },
- ondropdeactivate: event => {
- event.target.classList.remove('drop-active')
- },
- })
- },
- calcXY (top, left) {
- const colWidth = this.calcColWidth()
- let x = Math.round((left - this.colMargin[0]) / (colWidth + this.colMargin[0]))
- let y = Math.round((top - this.colMargin[1]) / (this.rowHeight + this.colMargin[1]))
- // Capping
- x = Math.max(Math.min(x, this.colNum - this.defaultGridW), 0)
- y = Math.max(Math.min(y, this.maxRows - this.defaultGridH), 0)
- return { x, y }
- },
- calcColWidth () {
- const placeholderGrid = this.$refs['grid-layout'].$children[0]
- return (placeholderGrid.containerWidth - (this.colMargin[0] * (this.colNum + 1))) / this.colNum
- },
- setCurrentOption (component) {
- this.currentOption = {
- component,
- ...this.extendGallery.extendsOptions[component],
- }
- },
- handleRemove (item) {
- const index = R.findIndex(R.propEq('i', item.i))(this.layout)
- this.layout.splice(index, 1)
- },
- handleCopy (item, params) {
- const id = `dashboard-item-${uuid(32)}`
- this.dashboardParams[id] = params
- this.layout.push({
- component: item.component,
- h: item.h,
- i: id,
- w: item.w,
- x: item.x,
- y: item.y,
- })
- },
- handleUpdateDashboardParams (key, params) {
- this.$set(this.dashboardParams, key, params)
- },
- updateDashboardOptions (id) {
- return new Promise((resolve, reject) => {
- const options = [...this.dashboardOptions]
- const index = R.findIndex(R.propEq('id', id))(options)
- if (index !== -1) {
- options[index].name = this.dashboardName
- } else {
- options.push({ id, name: this.dashboardName })
- }
- this.pm.update({
- id: `dashboard_${this.scope}`,
- data: {
- value: options,
- },
- }).then(() => {
- resolve()
- }).catch(error => {
- reject(error)
- })
- })
- },
- createNewDashboard (id) {
- return new Promise((resolve, reject) => {
- this.updateDashboardOptions(id).then(() => {
- this.pm.create({
- data: {
- name: id,
- value: this.genData(),
- },
- }).then(response => {
- resolve(response)
- }).catch(error => {
- reject(error)
- })
- }).catch(error => {
- reject(error)
- })
- })
- },
- async updateDashboard (id) {
- return new Promise((resolve, reject) => {
- this.updateDashboardOptions(id).then(() => {
- this.pm.update({
- id,
- data: {
- value: this.genData(),
- },
- }).then(response => {
- resolve(response)
- }).catch(error => {
- reject(error)
- })
- }).catch(error => {
- reject(error)
- })
- })
- },
- async handleConfirm () {
- if (!R.trim(this.dashboardName)) {
- this.$message.warn(this.$t('dashboard.text_120'))
- this.$refs.input.focus()
- return
- }
- this.submting = true
- this.isCheckSave = false
- try {
- let id = this.id
- let response
- if (this.isCreate) {
- id = `dashboard-${this.scope}-panel-${uuid(16)}`
- response = await this.createNewDashboard(id)
- } else {
- response = await this.updateDashboard(id)
- }
- if (response) {
- storage.set(`__oc_dashboard_${this.scope}__`, { id: response.data.name, name: this.dashboardName })
- }
- this.$router.push('/dashboard')
- } finally {
- this.submting = false
- }
- },
- // 设置视图所需的data
- setData (data) {
- const dashboardParams = {}
- const layout = []
- R.forEachObjIndexed((value, key) => {
- dashboardParams[key] = value.params
- layout.push({
- ...value.layout,
- i: key,
- })
- }, data)
- this.dashboardParams = dashboardParams
- this.layoutInit = R.clone(layout)
- this.layout = layout
- },
- // 生成需要存储到配置中的data
- genData () {
- const ret = {}
- const layouts = this.layout.filter(item => this.dashboardParams[item.i])
- for (let i = 0, len = layouts.length; i < len; i++) {
- const layout = layouts[i]
- ret[layout.i] = {
- layout: {
- x: layout.x,
- y: layout.y,
- w: layout.w,
- h: layout.h,
- minH: layout.minH,
- component: layout.component,
- },
- params: this.dashboardParams[layout.i],
- }
- }
- return ret
- },
- handleBack () {
- this.$router.push('/')
- },
- // 根据现有的dashboard名称,生成新的dashboard名称(dashboard-1,dashboard-2,...)
- genDashboardName () {
- const reg = /^dashboard-\d+$/g
- const numbers = []
- let max = 1
- R.forEach(item => {
- if (R.test(reg, item.name)) {
- const nameArr = item.name.split('-')
- numbers.push(parseInt(nameArr[1]))
- }
- }, this.dashboardOptions)
- if (numbers.length > 0) {
- max = Math.max(...numbers)
- max += 1
- }
- return `dashboard-${max}`
- },
- },
- }
- </script>
- <style lang="less">
- @import url('../../styles/index.less');
- </style>
- <style lang="less" scoped>
- .edit-wrap {
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- overflow: hidden;
- z-index: 0;
- }
- .edit-topbar {
- z-index: 5;
- box-shadow: 0 2px 4px 0 rgba(237, 237, 237, 0.5),
- 0 2px 4px 0 rgba(237, 237, 237, 0.5);
- height: 50px;
- }
- .edit-main {
- height: calc(100% - 40px);
- }
- .extend-gallery {
- z-index: 2;
- box-shadow: 4px 0px 5px 1px rgba(237, 237, 237, 0.5);
- }
- .edit-content {
- overflow: hidden;
- z-index: 1;
- }
- .edit-content-inner {
- top: 0;
- left: 0;
- z-index: 0;
- overflow: hidden;
- background-color: #f3f3f3;
- padding: 15px;
- }
- .edit-header {
- margin-left: 5px;
- }
- .edit-grid-item {
- user-select: none;
- background: rgba(0, 0, 0, 0.2);
- }
- ::v-deep .drop-active {
- border: 2px dashed skyblue;
- }
- ::v-deep .drop-active .vue-grid-item.vue-grid-placeholder {
- display: none;
- }
- </style>
|