推送项目重构代码
This commit is contained in:
91
ruoyi-ui/src/components/Workbench/WidgetWrapper.vue
Normal file
91
ruoyi-ui/src/components/Workbench/WidgetWrapper.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="widget-wrapper" :class="{ editing: editing }">
|
||||
<div class="widget-header">
|
||||
<span class="widget-title">{{ title }}</span>
|
||||
<span v-if="editing" class="widget-remove" @click="$emit('remove')">
|
||||
<i class="el-icon-close"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="widget-body">
|
||||
<component :is="component" v-if="component" />
|
||||
<div v-else class="widget-missing">未知组件: {{ widgetKey }}</div>
|
||||
</div>
|
||||
<div v-if="editing" class="widget-mask"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWidget } from './widgets/registry'
|
||||
|
||||
export default {
|
||||
name: 'WidgetWrapper',
|
||||
props: {
|
||||
widgetKey: { type: String, required: true },
|
||||
editing: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
widgetMeta () {
|
||||
return getWidget(this.widgetKey)
|
||||
},
|
||||
title () {
|
||||
return this.widgetMeta ? this.widgetMeta.title : this.widgetKey
|
||||
},
|
||||
component () {
|
||||
return this.widgetMeta ? this.widgetMeta.component : null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.widget-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.widget-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fafafa;
|
||||
}
|
||||
.widget-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.widget-remove {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
&:hover { color: #f56c6c; }
|
||||
}
|
||||
.widget-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.widget-missing {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.widget-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(64, 158, 255, 0.04);
|
||||
pointer-events: none;
|
||||
}
|
||||
.editing {
|
||||
border: 1px dashed #409EFF;
|
||||
}
|
||||
</style>
|
||||
270
ruoyi-ui/src/components/Workbench/index.vue
Normal file
270
ruoyi-ui/src/components/Workbench/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="workbench" v-loading="loading">
|
||||
<!-- 编辑入口:不在编辑态时只显示一个浮动小图标,不抢占整行 -->
|
||||
<button
|
||||
v-if="!editing"
|
||||
class="workbench-edit-fab"
|
||||
title="编辑工作台"
|
||||
@click="enterEdit"
|
||||
>
|
||||
<i class="el-icon-edit"></i>
|
||||
</button>
|
||||
|
||||
<!-- 编辑态浮动工具条 -->
|
||||
<div v-if="editing" class="workbench-edit-bar">
|
||||
<el-button type="success" size="mini" icon="el-icon-check" @click="save">保存</el-button>
|
||||
<el-button size="mini" icon="el-icon-close" @click="cancel">取消</el-button>
|
||||
<el-button type="warning" size="mini" icon="el-icon-refresh-left" @click="reset">重置</el-button>
|
||||
<el-button v-if="isAdmin" type="danger" size="mini" plain icon="el-icon-upload2" @click="saveAsDefault">设为全局默认</el-button>
|
||||
<el-button type="primary" size="mini" plain icon="el-icon-plus" @click="pickerVisible = true">添加组件</el-button>
|
||||
</div>
|
||||
|
||||
<grid-layout
|
||||
v-if="layout.length"
|
||||
:layout.sync="layout"
|
||||
:col-num="12"
|
||||
:row-height="30"
|
||||
:is-draggable="editing"
|
||||
:is-resizable="editing"
|
||||
:vertical-compact="true"
|
||||
:use-css-transforms="true"
|
||||
:margin="[12, 12]"
|
||||
>
|
||||
<grid-item
|
||||
v-for="item in layout"
|
||||
:key="item.i"
|
||||
:x="item.x"
|
||||
:y="item.y"
|
||||
:w="item.w"
|
||||
:h="item.h"
|
||||
:i="item.i"
|
||||
:min-w="2"
|
||||
:min-h="2"
|
||||
>
|
||||
<widget-wrapper
|
||||
:widget-key="item.widgetKey"
|
||||
:editing="editing"
|
||||
@remove="removeItem(item.i)"
|
||||
/>
|
||||
</grid-item>
|
||||
</grid-layout>
|
||||
|
||||
<div v-else-if="!loading" class="workbench-empty">
|
||||
工作台为空,<el-button type="text" @click="enterEdit">点击编辑</el-button>添加组件
|
||||
</div>
|
||||
|
||||
<!-- 添加组件抽屉 -->
|
||||
<el-drawer
|
||||
title="添加组件"
|
||||
:visible.sync="pickerVisible"
|
||||
direction="rtl"
|
||||
size="320px"
|
||||
>
|
||||
<div class="picker-list">
|
||||
<div
|
||||
v-for="w in availableWidgets"
|
||||
:key="w.key"
|
||||
class="picker-item"
|
||||
@click="addWidget(w)"
|
||||
>
|
||||
<span>{{ w.title }}</span>
|
||||
<i class="el-icon-plus"></i>
|
||||
</div>
|
||||
<div v-if="!availableWidgets.length" class="picker-empty">所有组件已添加</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GridLayout, GridItem } from 'vue-grid-layout'
|
||||
import WidgetWrapper from './WidgetWrapper.vue'
|
||||
import { listWidgets, getWidget } from './widgets/registry'
|
||||
import {
|
||||
getDashboardLayout,
|
||||
saveDashboardLayout,
|
||||
resetDashboardLayout,
|
||||
saveDefaultDashboardLayout
|
||||
} from '@/api/system/dashboard'
|
||||
|
||||
export default {
|
||||
name: 'Workbench',
|
||||
components: { GridLayout, GridItem, WidgetWrapper },
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
editing: false,
|
||||
layout: [],
|
||||
backupLayout: [],
|
||||
pickerVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
availableWidgets () {
|
||||
const used = new Set(this.layout.map(i => i.widgetKey))
|
||||
return listWidgets().filter(w => !used.has(w.key))
|
||||
},
|
||||
isAdmin () {
|
||||
const roles = this.$store.getters.roles || []
|
||||
return roles.includes('admin')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchLayout()
|
||||
},
|
||||
methods: {
|
||||
fetchLayout () {
|
||||
this.loading = true
|
||||
getDashboardLayout().then(res => {
|
||||
const raw = res && res.data && res.data.layout
|
||||
this.layout = this.parseLayout(raw)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
parseLayout (raw) {
|
||||
if (!raw) return []
|
||||
try {
|
||||
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
return Array.isArray(arr) ? arr : []
|
||||
} catch (e) {
|
||||
console.warn('parse dashboard layout failed', e)
|
||||
return []
|
||||
}
|
||||
},
|
||||
enterEdit () {
|
||||
this.backupLayout = JSON.parse(JSON.stringify(this.layout))
|
||||
this.editing = true
|
||||
},
|
||||
cancel () {
|
||||
this.layout = this.backupLayout
|
||||
this.editing = false
|
||||
},
|
||||
save () {
|
||||
const payload = JSON.stringify(this.layout)
|
||||
this.loading = true
|
||||
saveDashboardLayout(payload).then(() => {
|
||||
this.$modal.msgSuccess('工作台已保存')
|
||||
this.editing = false
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
reset () {
|
||||
this.$modal.confirm('确定重置为默认布局?当前自定义将丢失').then(() => {
|
||||
this.loading = true
|
||||
return resetDashboardLayout()
|
||||
}).then(() => {
|
||||
this.editing = false
|
||||
this.fetchLayout()
|
||||
this.$modal.msgSuccess('已重置为默认布局')
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
removeItem (i) {
|
||||
this.layout = this.layout.filter(item => item.i !== i)
|
||||
},
|
||||
saveAsDefault () {
|
||||
this.$modal.confirm('确认将当前布局设为全局默认?所有未自定义过工作台的用户都会看到此布局').then(() => {
|
||||
const payload = JSON.stringify(this.layout)
|
||||
this.loading = true
|
||||
return saveDefaultDashboardLayout(payload)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已保存为全局默认布局')
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
addWidget (w) {
|
||||
const meta = getWidget(w.key)
|
||||
const size = (meta && meta.defaultSize) || { w: 6, h: 6 }
|
||||
const maxY = this.layout.reduce((m, it) => Math.max(m, it.y + it.h), 0)
|
||||
this.layout.push({
|
||||
i: w.key,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: size.w,
|
||||
h: size.h,
|
||||
widgetKey: w.key
|
||||
})
|
||||
this.pickerVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workbench {
|
||||
padding: 8px 12px 12px;
|
||||
min-height: calc(100vh - 68px);
|
||||
position: relative;
|
||||
}
|
||||
.workbench-edit-fab {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 18px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
color: #409EFF;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
transition: all .2s;
|
||||
opacity: 0.6;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.workbench-edit-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.workbench-empty {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
.picker-list {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.picker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
color: #409EFF;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
.picker-empty {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
81
ruoyi-ui/src/components/Workbench/widgets/registry.js
Normal file
81
ruoyi-ui/src/components/Workbench/widgets/registry.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Widget 注册表:key -> { title, component, defaultSize }
|
||||
// 所有可放入工作台的组件统一在此登记。新增组件只需在此添加一项即可。
|
||||
// 栅格 col-num = 12,三列 ≈ w:4
|
||||
|
||||
import Announcements from '@/components/Announcements/index.vue'
|
||||
import MiniCalendar from '@/components/MiniCalendar/index.vue'
|
||||
import QuickEntry from '@/components/QuickEntry/index.vue'
|
||||
import {
|
||||
ExpressQuestionList,
|
||||
FeedbackList,
|
||||
FinancialCharts,
|
||||
MyTaskList,
|
||||
OwnerTaskList,
|
||||
ProjectManagement,
|
||||
RequirementList
|
||||
} from '@/components/HomeModules/index'
|
||||
|
||||
export const WIDGET_REGISTRY = {
|
||||
announcements: {
|
||||
title: '通知公告',
|
||||
component: Announcements,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
projectManagement: {
|
||||
title: '项目管理',
|
||||
component: ProjectManagement,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
ownerTaskList: {
|
||||
title: '分配我的任务',
|
||||
component: OwnerTaskList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
myTaskList: {
|
||||
title: '我发放的任务',
|
||||
component: MyTaskList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
financialCharts: {
|
||||
title: '财务图表',
|
||||
component: FinancialCharts,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
feedbackList: {
|
||||
title: '问题反馈',
|
||||
component: FeedbackList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
requirementList: {
|
||||
title: '需求下发',
|
||||
component: RequirementList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
expressQuestionList: {
|
||||
title: '快递问题',
|
||||
component: ExpressQuestionList,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
miniCalendar: {
|
||||
title: '日程日历',
|
||||
component: MiniCalendar,
|
||||
defaultSize: { w: 4, h: 8 }
|
||||
},
|
||||
quickEntry: {
|
||||
title: '快捷入口',
|
||||
component: QuickEntry,
|
||||
defaultSize: { w: 12, h: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
export function getWidget(key) {
|
||||
return WIDGET_REGISTRY[key]
|
||||
}
|
||||
|
||||
export function listWidgets() {
|
||||
return Object.keys(WIDGET_REGISTRY).map(key => ({
|
||||
key,
|
||||
title: WIDGET_REGISTRY[key].title,
|
||||
defaultSize: WIDGET_REGISTRY[key].defaultSize
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user