推送项目重构代码

This commit is contained in:
2026-05-29 19:52:32 +08:00
parent 95141d0e1f
commit 3dafaceef2
65 changed files with 3762 additions and 583 deletions

View 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>

View 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>

View 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
}))
}