feat: 完善批量修改填写人、审核页面优化、人员管理等功能

- 批量修改填写人:添加左侧分类列表、折叠功能、暂存和重新分配
- 审核页面:添加折叠和分页功能
- 查询分析页面:添加单位列显示
- 人员管理:创建独立页面,支持搜索、分页、批量操作
- 修复各种UI和功能问题
This commit is contained in:
Zhou 2026-06-23 09:55:55 +08:00
parent 4e1bef2888
commit 46b01b4504
212 changed files with 96867 additions and 79912 deletions

4
.arts/settings.json Normal file
View File

@ -0,0 +1,4 @@
{
"clawMode.mode": "editor",
"workbench.activityBar.location": "default"
}

2
.env
View File

@ -1,3 +1,3 @@
VUE_APP_BASE_DOMAIN=http://dcloud-demo.definesys.cn:31213
VUE_APP_TENANT_ID=254638499280453633
VUE_APP_PUBLIC_PATH=/app/
VUE_APP_PUBLIC_PATH=/

View File

@ -1,36 +0,0 @@
# yuchai
#### Description
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@ -1,39 +0,0 @@
# yuchai
#### 介绍
{**以下是 Gitee 平台说明,您可以替换此简介**
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN。专为开发者提供稳定、高效、安全的云端软件开发协作平台
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -142,6 +142,13 @@
"title": "权限管理"
}
},
"apaas-custom-roleMemberManagement":{
"name": "apaas-custom-roleMemberManagement",
"path": "apaas-custom-roleMemberManagement",
"meta": {
"title": "人员管理"
}
},
"apaas-custom-checkPersonPermission":{
"name": "apaas-custom-checkPersonPermission",
"path": "apaas-custom-checkPersonPermission",

37797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,6 @@
"eslint-plugin-vue": "^6.2.2",
"husky": "^4.2.5",
"lint-staged": "^9.5.0",
"node-sass": "^4.12.0",
"path": "^0.12.7",
"prettier": "^1.19.1",
"progress-bar-webpack-plugin": "^2.1.0",

View File

@ -134,5 +134,17 @@ export default {
GET_DEPT_OF_USER: {
url: '/custom/responsible/getResponsibleDepartment',
method: 'get'
},
UPDATE_TEMPLATE_ORDER: {
url: '/custom/template/batchUpdateTemplateOrder',
method: 'post'
},
BATCH_UPDATE_DEPARTMENT: {
url: '/custom/todoTaskPojo/batchUpdateDepartment',
method: 'post'
},
SAVE_FILLED_BY_TEMP: {
url: '/custom/engineParamDetailPojo/saveFilledByTemp',
method: 'post'
}
}

View File

@ -64,5 +64,20 @@ export default {
GET_ROLE_CONTROL_LIST: {
url: '/custom/permission/selectPersByRole',
method: 'get'
},
// 获取角色成员列表
GET_ROLE_MEMBERS: {
url: '/custom/permission/getRoleMembers',
method: 'get'
},
// 添加角色成员
ADD_ROLE_MEMBERS: {
url: '/custom/permission/addRoleMembers',
method: 'post'
},
// 删除角色成员
DELETE_ROLE_MEMBERS: {
url: '/custom/permission/deleteRoleMembers',
method: 'post'
}
}

View File

@ -142,6 +142,13 @@
"title": "权限管理"
}
},
"apaas-custom-roleMemberManagement":{
"name": "apaas-custom-roleMemberManagement",
"path": "apaas-custom-roleMemberManagement",
"meta": {
"title": "人员管理"
}
},
"apaas-custom-checkPersonPermission":{
"name": "apaas-custom-checkPersonPermission",
"path": "apaas-custom-checkPersonPermission",

View File

@ -32,10 +32,11 @@
</div>
<div class="detail-content">
<section class="block-section">
<div class="section-title">
<div class="section-title" @click="toggleBasicInfo">
基础信息
<i :class="basicInfoCollapsed ? 'el-icon-caret-left' : 'el-icon-caret-bottom'" class="collapse-icon"></i>
</div>
<div class="info-grid">
<div v-show="!basicInfoCollapsed" class="info-grid">
<!-- 流程主题单独一行 -->
<div class="info-item-full">
<span class="label">流程主题</span>
@ -87,10 +88,11 @@
</div>
</section>
<section class="block-section">
<div class="section-title">
<div class="section-title" @click="toggleDetailInfo">
详细信息
<i :class="detailInfoCollapsed ? 'el-icon-caret-left' : 'el-icon-caret-bottom'" class="collapse-icon"></i>
</div>
<div class="info-grid">
<div v-show="!detailInfoCollapsed" class="info-grid">
<div class="info-row">
<div class="info-item">
<span class="label">产品型号</span>
@ -157,10 +159,12 @@
</div>
</section>
<section class="block-section">
<div class="section-title">
<div class="section-title" @click="toggleApprovalContent">
审批内容
<i :class="approvalContentCollapsed ? 'el-icon-caret-left' : 'el-icon-caret-bottom'" class="collapse-icon"></i>
</div>
<el-table :data="changeTable || []" border class="change-table" :max-height="500">
<div v-show="!approvalContentCollapsed">
<el-table :data="changeTable || []" border class="change-table" :max-height="500">
<el-table-column label="序号" type="index" width="60" />
<el-table-column
prop="subsystemName"
@ -192,26 +196,31 @@
<el-table-column prop="changeComment" label="修改描述" min-width="160">
</el-table-column>
</el-table>
<el-pagination
v-if="changeTableTotal > 0"
class="pagination-wrapper"
:current-page="changeTablePageNumber"
:page-size="changeTablePageSize"
:total="changeTableTotal"
layout="total, prev, pager, next, jumper"
@current-change="handlePageChange"
/>
<div class="pagination-wrapper">
<el-pagination
:current-page="changeTablePageNumber"
:page-size="changeTablePageSize"
:total="changeTableTotal"
layout="total, prev, pager, next, jumper, sizes"
:page-sizes="[10, 20, 50, 100, 9999]"
popper-class="pagination-select-dropdown"
@current-change="handlePageChange"
@size-change="handlePageSizeChange"
/>
</div>
<div v-if="progressDetail.description" class="description-block">
<span class="label">说明</span>
<span class="value">{{ detailOption }}</span>
</div>
</div>
</section>
<section class="block-section">
<div class="section-title">
<div class="section-title" @click="toggleApprovalFlow">
审批流程
<i :class="approvalFlowCollapsed ? 'el-icon-caret-left' : 'el-icon-caret-bottom'" class="collapse-icon"></i>
</div>
<div v-if="timelineNodes && timelineNodes.length > 0" class="approval-timeline">
<div v-show="!approvalFlowCollapsed" v-if="timelineNodes && timelineNodes.length > 0" class="approval-timeline">
<el-timeline>
<el-timeline-item
v-for="(node, index) in timelineNodes"
@ -247,7 +256,7 @@
</el-timeline-item>
</el-timeline>
</div>
<div v-else class="empty-progress">
<div v-show="!approvalFlowCollapsed" v-else class="empty-progress">
<span class="empty-text">暂无审批流程数据</span>
</div>
</section>
@ -369,6 +378,10 @@ export default {
},
data() {
return {
basicInfoCollapsed: true, //
detailInfoCollapsed: false, //
approvalContentCollapsed: false, //
approvalFlowCollapsed: true, //
changeTable: [],
showTopDetail: {
processTitle: '',
@ -422,6 +435,9 @@ export default {
}
},
computed: {
changeTablePage() {
return this.changeTablePageNumber
},
titleText() {
console.log(this.engineType, 'this.engineType', this.detail, 111111)
return this.engineType === '玉柴' ? '玉柴发动机审批详情' : '竞品发动机审批详情'
@ -617,6 +633,18 @@ export default {
// this.getApprovalChange()
},
methods: {
toggleBasicInfo() {
this.basicInfoCollapsed = !this.basicInfoCollapsed
},
toggleDetailInfo() {
this.detailInfoCollapsed = !this.detailInfoCollapsed
},
toggleApprovalContent() {
this.approvalContentCollapsed = !this.approvalContentCollapsed
},
toggleApprovalFlow() {
this.approvalFlowCollapsed = !this.approvalFlowCollapsed
},
canResendApproval() {
const request = {
...api.CHECK_DATA_CAN_RETURN,
@ -1253,6 +1281,11 @@ export default {
//
this.updatePagedData()
},
handlePageSizeChange(size) {
this.changeTablePageSize = size
this.changeTablePageNumber = 1
this.updatePagedData()
},
changeTypeClass(type) {
if (type === 'create') return 'change-create'
if (type === 'delete') return 'change-delete'
@ -1486,6 +1519,21 @@ export default {
font-weight: 600;
color: #303133;
margin-bottom: 16px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 8px;
.collapse-icon {
font-size: 14px;
color: #909399;
transition: transform 0.3s;
&:hover {
color: #409eff;
}
}
}
}
@ -1623,8 +1671,22 @@ export default {
.pagination-wrapper {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 16px;
margin-bottom: 16px;
padding-right: 0;
::v-deep .el-pagination {
display: flex;
justify-content: flex-end;
align-items: center;
.el-select {
.el-input {
width: 100px !important;
}
}
}
}
.modify-details-text {
@ -1882,3 +1944,21 @@ export default {
line-height: 0.85 !important;
}
</style>
<style lang="scss">
.pagination-select-dropdown {
width: 110px !important;
min-width: 110px !important;
max-width: 110px !important;
.el-select-dropdown__wrap {
max-height: 200px;
}
.el-select-dropdown__item {
width: 100%;
text-align: left;
padding: 0 10px;
}
}
</style>

View File

@ -25,6 +25,13 @@
</div>
<div v-if="!isApprovalMode && !hideOperations" class="operation">
<template v-if="isDispatchMode">
<el-button
v-if="dispatchType !== 'update_param'"
size="small"
@click="handleBatchModifyDept"
>
批量修改部门
</el-button>
<el-button
v-if="shouldShowRejectButton"
size="small"
@ -62,12 +69,12 @@
<el-button size="small" v-if="!isEditDeptMode && hasAnyEditPermission() && detail &&( detail.distributeStatus === '已完成' || detail.distributeStatus === '未分发')" @click="handleEditDept">
编辑部门
</el-button>
<el-button
v-if="!isEditMode && !isEditDeptMode && hasAnyEditPermission() && detail && detail.distributeStatus === '已完成'"
<!-- <el-button
v-if="!isEditMode && !isEditDeptMode && hasAnyEditPermission() && (detail && detail.distributeStatus === '已完成' || fromTodo)"
size="small"
@click="handleEdit">
编辑
</el-button>
</el-button> -->
<el-button
v-if="!isEditMode && !isEditDeptMode && !isJustCheckMode"
size="small"
@ -194,8 +201,11 @@
<!-- 发动机基本信息表格 -->
<template v-for="(sectionData, sectionKey) in wholeEngineParamsData">
<div v-if="isWholeEngineParamsSelected" class="section" :key="sectionKey">
<div class="section-title">{{ sectionKey }}</div>
<div class="grid whole-engine-grid">
<div class="section-title" @click="detailInfoCollapsed = !detailInfoCollapsed" style="cursor: pointer;">
<span>{{ sectionKey }}</span>
<i :class="detailInfoCollapsed ? 'el-icon-arrow-right' : 'el-icon-arrow-down'" style="margin-left: 8px;"></i>
</div>
<div class="grid whole-engine-grid" v-show="!detailInfoCollapsed">
<div
class="grid-header whole-engine-header"
:class="{ 'without-parameter-source': isCompetitiveEngineEntry }"
@ -226,6 +236,9 @@
</div>
</template>
<!-- 子系统参数表格 -->
<div class="params-control" style="margin-bottom: 10px; text-align: right;" v-if="!isWholeEngineParamsSelected">
<el-checkbox v-model="showAllParams">全部显示不分页</el-checkbox>
</div>
<template v-for="(sectionData, sectionKey) in display">
<div class="section" v-if="shouldShowSection(sectionKey)" :key="sectionKey">
<div class="section-title">{{ formatSectionTitle(sectionKey, sectionData) }}</div>
@ -376,10 +389,11 @@
<div v-if="isApprovalMode" class="page-approval-content">
<!-- 基础信息 -->
<section v-if="detail" class="block-section">
<div class="section-title">
基础信息
<div class="section-title" @click="basicInfoCollapsed = !basicInfoCollapsed" style="cursor: pointer;">
<span>基础信息</span>
<i :class="basicInfoCollapsed ? 'el-icon-arrow-right' : 'el-icon-arrow-down'" style="margin-left: 8px;"></i>
</div>
<div class="info-grid">
<div class="info-grid" v-show="!basicInfoCollapsed">
<!-- 流程主题单独一行 -->
<div class="info-item-full">
<span class="label">流程主题 <span class="required-mark">*</span></span>
@ -476,10 +490,11 @@
<!-- 审批环节 -->
<div class="approval-workflow">
<div class="workflow-title">
审批环节
<div class="workflow-title" @click="approvalWorkflowCollapsed = !approvalWorkflowCollapsed" style="cursor: pointer;">
<span>审批环节</span>
<i :class="approvalWorkflowCollapsed ? 'el-icon-arrow-right' : 'el-icon-arrow-down'" style="margin-left: 8px;"></i>
</div>
<div class="workflow-table">
<div class="workflow-table" v-show="!approvalWorkflowCollapsed">
<table class="workflow-table-content">
<thead>
<tr>
@ -558,10 +573,147 @@
:resolve-department-id="resolveDepartmentId"
:project-parts-name="treeFirstLevelPartsName"
:tree-subsystem-names="treeSubsystemNames"
:dispatch-param-draft-cache="dispatchParamDraftCache"
@confirm="handleBatchModifyConfirm"
@refresh-responsible-persons="loadResponsiblePersons"
@refresh-department="handleRefreshDepartment"
@update-draft-cache="handleUpdateDraftCache"
/>
<!-- 修改部门弹窗 -->
<el-dialog
title="修改部门"
:visible.sync="batchModifyDeptDialogVisible"
width="600px"
:close-on-click-modal="false"
append-to-body
@close="handleBatchModifyDeptDialogClose"
>
<el-form ref="batchModifyDeptForm" :model="batchModifyDeptFormData" label-width="110px">
<el-form-item label="原部门:">
<el-input
v-model="batchModifyDeptFormData.originalDept"
placeholder="请选择原部门"
readonly
style="width: 100%"
@click.native="handleSelectOriginalDept"
>
<el-button slot="append" icon="el-icon-search" @click="handleSelectOriginalDept"></el-button>
</el-input>
</el-form-item>
<el-form-item label="部门:">
<el-input
v-model="batchModifyDeptFormData.targetDept"
placeholder="请选择部门"
readonly
style="width: 100%"
@click.native="handleSelectTargetDept"
>
<el-button slot="append" icon="el-icon-search" @click="handleSelectTargetDept"></el-button>
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleBatchModifyDeptDialogClose">
取消
</el-button>
<el-button
type="primary"
:loading="batchModifyDeptSubmitting"
:disabled="!batchModifyDeptFormData.originalDept || !batchModifyDeptFormData.targetDept"
@click="handleBatchModifyDeptConfirm"
>
确定
</el-button>
</div>
</el-dialog>
<!-- 原填写部门选择弹窗 -->
<el-dialog
title="选择原填写部门"
:visible.sync="originalDeptDialogVisible"
width="500px"
:close-on-click-modal="false"
append-to-body
@close="handleOriginalDeptDialogClose"
>
<el-input
v-model="originalDeptTreeFilterText"
size="small"
placeholder="请输入部门名称进行搜索"
clearable
class="department-tree-search"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<el-tree
ref="originalDeptTree"
:data="departmentList"
:props="{ children: 'children', label: 'name' }"
node-key="id"
:filter-node-method="filterDepartmentNode"
highlight-current
:expand-on-click-node="false"
class="department-tree"
@node-click="handleOriginalDeptNodeClick"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="handleOriginalDeptDialogClose">
取消
</el-button>
<el-button
type="primary"
:disabled="!selectedOriginalDeptId"
@click="handleOriginalDeptDialogConfirm"
>
确定
</el-button>
</div>
</el-dialog>
<!-- 填写部门选择弹窗 -->
<el-dialog
title="选择填写部门"
:visible.sync="targetDeptDialogVisible"
width="500px"
:close-on-click-modal="false"
append-to-body
@close="handleTargetDeptDialogClose"
>
<el-input
v-model="targetDeptTreeFilterText"
size="small"
placeholder="请输入部门名称进行搜索"
clearable
class="department-tree-search"
>
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<el-tree
ref="targetDeptTree"
:data="departmentList"
:props="{ children: 'children', label: 'name' }"
node-key="id"
:filter-node-method="filterDepartmentNode"
highlight-current
:expand-on-click-node="false"
class="department-tree"
@node-click="handleTargetDeptNodeClick"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="handleTargetDeptDialogClose">
取消
</el-button>
<el-button
type="primary"
:disabled="!selectedTargetDeptId"
@click="handleTargetDeptDialogConfirm"
>
确定
</el-button>
</div>
</el-dialog>
<!-- 竞品发动机修改全部填写人/责任人仅选人不触发修改责任人接口 -->
<el-dialog
:title="dispatchType === 'update_param' ? '修改全部填写人' : '修改全部责任人'"
@ -611,76 +763,7 @@
</span>
</el-dialog>
<!-- 批量修改部门弹窗 -->
<el-dialog
title="批量修改部门"
:visible.sync="batchModifyDeptDialogVisible"
width="600px"
append-to-body
@close="handleBatchModifyDeptDialogClose"
>
<el-form
ref="batchModifyDeptForm"
:model="batchModifyDeptFormData"
:rules="batchModifyDeptFormRules"
label-width="120px"
>
<el-form-item label="子系统:" prop="subsystem">
<el-select
v-model="batchModifyDeptFormData.subsystem"
placeholder="请选择子系统"
clearable
style="width: 100%"
@change="handleBatchModifyDeptSubsystemChange"
>
<el-option
v-for="item in batchModifyDeptSubsystemList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="零部件名称:" prop="partsName">
<el-select
v-model="batchModifyDeptFormData.partsName"
placeholder="请选择零部件名称"
clearable
multiple
style="width: 100%"
:disabled="!batchModifyDeptFormData.subsystem"
>
<el-option
v-for="item in batchModifyDeptPartsNameList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="部门:" prop="department">
<el-select
v-model="batchModifyDeptFormData.department"
placeholder="请选择部门"
clearable
filterable
style="width: 100%"
:filter-method="handleBatchModifyDeptRemoteSearch"
>
<el-option
v-for="dept in filteredBatchModifyDeptOptions"
:key="dept.id"
:value="dept.name"
:label="dept.name"
/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="handleBatchModifyDeptDialogClose">取消</el-button>
<el-button type="primary" :loading="batchModifyDeptSubmitting" @click="handleBatchModifyDeptConfirm">确定</el-button>
</span>
</el-dialog>
</div>
</template>
@ -810,6 +893,24 @@ export default {
},
replaceAllPersonUserOptions: [],
replaceAllPersonUserLoading: false,
//
batchModifyDeptDialogVisible: false,
batchModifyDeptSubmitting: false,
batchModifyDeptFormData: {
originalDept: '',
originalDeptId: '',
targetDept: '',
targetDeptId: ''
},
originalDeptDialogVisible: false,
originalDeptTreeFilterText: '',
selectedOriginalDeptId: '',
selectedOriginalDeptName: '',
targetDeptDialogVisible: false,
targetDeptTreeFilterText: '',
selectedTargetDeptId: '',
selectedTargetDeptName: '',
// ID
versions: [],
treeData: [],
@ -817,6 +918,10 @@ export default {
currentEngineModelID: '', // ID
versionData: {},
changeData: [], //
basicInfoCollapsed: false, //
detailInfoCollapsed: false, //
approvalWorkflowCollapsed: true, //
showAllParams: false, //
importing: false, //
isApprovalMode: false, //
engineDetailInfo: null, // modelName projectNumber
@ -880,6 +985,16 @@ export default {
console.log('data', data)
return data
},
//
displayParams() {
const data = this.display
if (this.showAllParams) {
return data
}
//
//
return data
},
currentVersionLabel() {
const version = this.versions.find((v) => v.id === this.currentVersionId)
return version ? version.label : 'V1'
@ -1016,6 +1131,22 @@ export default {
})
return names
},
//
subsystemList() {
const currentData = this.versionData[this.currentVersionId] || {}
const subsystemSet = new Set()
Object.keys(currentData).forEach((partsName) => {
const params = currentData[partsName] || []
params.forEach((param) => {
if (param.subsystemName) {
subsystemSet.add(param.subsystemName)
}
})
})
return Array.from(subsystemSet).sort()
},
// justCheck
isJustCheckMode() {
return this.dispatchType === 'justCheck'
@ -1179,10 +1310,25 @@ export default {
},
// 退
canShowTodoHeaderOperations() {
console.log('canShowTodoHeaderOperations - isFillerTodoEntry:', this.isFillerTodoEntry)
console.log('canShowTodoHeaderOperations - fromTodo:', this.fromTodo)
console.log('canShowTodoHeaderOperations - detail:', this.detail)
console.log('canShowTodoHeaderOperations - distributeStatus:', this.detail && this.detail.distributeStatus)
if (this.isFillerTodoEntry) {
console.log('canShowTodoHeaderOperations - 返回 true (isFillerTodoEntry)')
return true
}
return !this.detail || this.detail.distributeStatus !== '已完成'
// 退
if (this.fromTodo) {
console.log('canShowTodoHeaderOperations - 返回 true (fromTodo)')
return true
}
// 使访
const distributeStatus = this.detail && this.detail.distributeStatus
const result = !this.detail || distributeStatus !== '已完成'
console.log('canShowTodoHeaderOperations - 返回:', result)
return result
},
// "退"
shouldShowRejectButton() {
@ -1315,6 +1461,20 @@ export default {
this.loadResponsiblePersons()
this.loadFillerPersons()
}
},
originalDeptTreeFilterText(val) {
this.$nextTick(() => {
if (this.$refs.originalDeptTree) {
this.$refs.originalDeptTree.filter(val)
}
})
},
targetDeptTreeFilterText(val) {
this.$nextTick(() => {
if (this.$refs.targetDeptTree) {
this.$refs.targetDeptTree.filter(val)
}
})
}
},
created() {
@ -1735,11 +1895,13 @@ export default {
},
// 使
hasAnyEditPermission() {
//
//
const dataTypeStr = String(this.detail.dataType || '')
console.log(dataTypeStr)
if (
this.fromTodo &&
this.detail &&
String(this.detail.dataType || '') === '数据维护'
(dataTypeStr === '数据维护' || dataTypeStr === '数据拒绝')
) {
return true
}
@ -1765,7 +1927,7 @@ export default {
const hasGlobalEdit = permissionData.some((perm) => {
const permissionPath = perm.permissionPath || ''
const parameterPer = perm.parameterPer || ''
// permissionPath engineTypePath parameterPer ""
// permissionPath engineTypePath parameterPer ""
return permissionPath === engineTypePath && parameterPer === '编辑'
})
@ -1782,6 +1944,7 @@ export default {
return parameterPer === '编辑' && permissionPath.startsWith(engineTypePath + '/')
})
return hasAnyEdit
} catch (err) {
console.error('检查编辑权限失败:', err)
@ -1797,30 +1960,34 @@ export default {
return (person.partsName || '') === treePartsName
},
//
//
isResponsiblePersonSubsystemDeptMatch(person, subsystemName, department) {
if (!person) {
return false
}
let subsystemMatch = false
if (subsystemName && person.subsystem) {
const personSubsystems = person.subsystem.split(',').map((s) => s.trim())
subsystemMatch = personSubsystems.includes(subsystemName)
} else if (!subsystemName && !person.subsystem) {
subsystemMatch = true
}
//
let departmentMatch = false
if (department && person.department) {
const normalizedParamDept = department.replace(/·/g, '-')
const normalizedPersonDept = person.department.replace(/·/g, '-')
departmentMatch =
//
const exactMatch =
normalizedPersonDept === normalizedParamDept ||
normalizedPersonDept.startsWith(normalizedParamDept + '-')
//
const aliasMatch =
this.isZQOrZZPowerPlatformDept(department) &&
this.isZQOrZZPowerPlatformDept(person.department)
departmentMatch = exactMatch || aliasMatch
} else if (!department && !person.department) {
departmentMatch = true
}
return subsystemMatch && departmentMatch
return departmentMatch
},
// GET_MANAGER_DATA
loadResponsiblePersons() {
@ -2162,6 +2329,12 @@ export default {
}
this.saveDispatchParamDraft(row)
},
// draft
handleUpdateDraftCache(draftKey, draft) {
if (draftKey && draft) {
this.$set(this.dispatchParamDraftCache, draftKey, draft)
}
},
//
getFilteredPersonsByDepartment(department) {
if (!department) {
@ -2990,6 +3163,227 @@ export default {
//
this.$refs.batchModifyResponsibleModal.showModal()
},
//
handleBatchModifyDeptDialogClose() {
this.batchModifyDeptDialogVisible = false
this.batchModifyDeptFormData = {
originalDept: '',
originalDeptId: '',
targetDept: '',
targetDeptId: ''
}
},
//
handleBatchModifyDeptConfirm() {
if (!this.batchModifyDeptFormData.originalDept || !this.batchModifyDeptFormData.targetDept) {
this.$message.warning('请选择原部门和部门')
return
}
if (this.batchModifyDeptFormData.originalDept === this.batchModifyDeptFormData.targetDept) {
this.$message.warning('原部门和部门不能相同')
return
}
// versionData
const currentVersionData = this.versionData[this.currentVersionId] || {}
//
const existingDepts = new Set()
Object.keys(currentVersionData).forEach((partsName) => {
const params = currentVersionData[partsName] || []
params.forEach((param) => {
if (param.department) {
existingDepts.add(param.department)
}
})
})
const existingDeptsArray = Array.from(existingDepts)
//
const originalDeptExists = existingDeptsArray.includes(this.batchModifyDeptFormData.originalDept)
if (!originalDeptExists) {
//
const message = `原部门【${this.batchModifyDeptFormData.originalDept}】不存在于当前数据中。\n当前存在的部门${existingDeptsArray.join('、')}`
this.$message.warning(message)
return
}
//
const itemsToUpdate = []
Object.keys(currentVersionData).forEach((partsName) => {
const params = currentVersionData[partsName] || []
params.forEach((param) => {
// 使
if (this.isDepartmentMatched(param.department, this.batchModifyDeptFormData.originalDept)) {
if (param.id) {
itemsToUpdate.push({
id: param.id,
department: this.batchModifyDeptFormData.targetDept
})
}
}
})
})
//
if (itemsToUpdate.length === 0) {
this.$message.warning('未找到匹配的参数数据')
return
}
//
const request = {
...api.BATCH_UPDATE_DEPARTMENT,
params: {
items: itemsToUpdate
}
}
this.batchModifyDeptSubmitting = true
this.$request(request)
.asyncThen((resp) => {
//
Object.keys(currentVersionData).forEach((partsName) => {
const params = currentVersionData[partsName] || []
params.forEach((param, index) => {
if (this.isDepartmentMatched(param.department, this.batchModifyDeptFormData.originalDept)) {
//
param.department = this.batchModifyDeptFormData.targetDept
//
const matchedPerson = this.responsiblePersons.find((person) => {
return this.isResponsiblePersonSubsystemDeptMatch(person, param.subsystemName, param.department)
})
if (matchedPerson) {
//
param.resPersonId = matchedPerson.userId || matchedPerson.id || ''
param.owner = matchedPerson.userName || matchedPerson.username || ''
}
}
})
})
this.batchModifyDeptSubmitting = false
this.batchModifyDeptDialogVisible = false
this.$message.success(`成功更新 ${itemsToUpdate.length} 个参数的部门`)
})
.asyncErrorCatch((err) => {
console.error('批量更新部门失败:', err)
this.$message.error('批量更新部门失败,请重试')
this.batchModifyDeptSubmitting = false
})
},
//
isDepartmentMatched(deptA, deptB) {
if (!deptA || !deptB) {
return false
}
//
const normalizeDept = (dept) => {
if (!dept) return ''
return dept.replace(/·/g, '-').trim()
}
const normalizedA = normalizeDept(deptA)
const normalizedB = normalizeDept(deptB)
//
if (normalizedA === normalizedB) {
return true
}
//
const aliasGroups = [
['中轻', '中重动力平台', '平台部', '中轻动力平台部', '中重动力平台部']
]
for (const group of aliasGroups) {
if (group.some(alias => normalizedA.includes(alias)) && group.some(alias => normalizedB.includes(alias))) {
return true
}
}
return false
},
//
handleSelectOriginalDept() {
this.originalDeptDialogVisible = true
this.selectedOriginalDeptId = this.batchModifyDeptFormData.originalDeptId || ''
this.selectedOriginalDeptName = this.batchModifyDeptFormData.originalDept || ''
this.originalDeptTreeFilterText = ''
this.$nextTick(() => {
if (this.$refs.originalDeptTree && this.selectedOriginalDeptId) {
this.$refs.originalDeptTree.setCurrentKey(this.selectedOriginalDeptId)
}
})
},
//
handleOriginalDeptDialogClose() {
this.originalDeptDialogVisible = false
this.selectedOriginalDeptId = ''
this.selectedOriginalDeptName = ''
},
//
handleOriginalDeptNodeClick(data) {
this.selectedOriginalDeptId = data.id
this.selectedOriginalDeptName = data.name
},
//
handleOriginalDeptDialogConfirm() {
if (!this.selectedOriginalDeptId) {
this.$message.warning('请选择部门')
return
}
this.batchModifyDeptFormData.originalDept = this.selectedOriginalDeptName
this.batchModifyDeptFormData.originalDeptId = this.selectedOriginalDeptId
this.originalDeptDialogVisible = false
},
//
handleSelectTargetDept() {
this.targetDeptDialogVisible = true
this.selectedTargetDeptId = this.batchModifyDeptFormData.targetDeptId || ''
this.selectedTargetDeptName = this.batchModifyDeptFormData.targetDept || ''
this.targetDeptTreeFilterText = ''
this.$nextTick(() => {
if (this.$refs.targetDeptTree && this.selectedTargetDeptId) {
this.$refs.targetDeptTree.setCurrentKey(this.selectedTargetDeptId)
}
})
},
//
handleTargetDeptDialogClose() {
this.targetDeptDialogVisible = false
this.selectedTargetDeptId = ''
this.selectedTargetDeptName = ''
},
//
handleTargetDeptNodeClick(data) {
this.selectedTargetDeptId = data.id
this.selectedTargetDeptName = data.name
},
//
handleTargetDeptDialogConfirm() {
if (!this.selectedTargetDeptId) {
this.$message.warning('请选择部门')
return
}
this.batchModifyDeptFormData.targetDept = this.selectedTargetDeptName
this.batchModifyDeptFormData.targetDeptId = this.selectedTargetDeptId
this.targetDeptDialogVisible = false
},
//
handleRefreshDepartment() {
//
if (this.checkedKeys && this.checkedKeys.length > 0) {
this.loadTreeData()
}
},
// /
handleReplaceAllPerson() {
this.replaceAllPersonDialogVisible = true
@ -3223,10 +3617,22 @@ export default {
const subsystemMatched = selectedSubsystems.some((name) =>
this.isSubsystemNameMatch(param.subsystemName, name)
)
// "/"
let partsNameMatched = false
if (paramPartsName === '/') {
// "/" partsName
const displayName = `${param.subsystemName} - /`
partsNameMatched = partsName.includes(displayName)
} else {
// "/"
partsNameMatched = partsName.includes(paramPartsName)
}
// isDistributedFilled === 1
if (
subsystemMatched &&
partsName.includes(paramPartsName) &&
partsNameMatched &&
param.isDistributedFilled !== 1
) {
//
@ -3390,6 +3796,23 @@ export default {
fieldValue = currentParam.resPersonId || ''
if (fieldValue) {
shouldDispatch = true
//
const responsiblePerson = this.responsiblePersons.find(p => p.userId === fieldValue)
if (responsiblePerson && currentParam.department) {
const paramDept = this.formatDepartmentDisplay(currentParam.department)
const personDept = this.formatDepartmentDisplay(responsiblePerson.department || '')
//
if (paramDept !== personDept) {
// /
const isAliasMatch =
(this.isZQOrZZPowerPlatformDept(paramDept) && this.isZQOrZZPowerPlatformDept(personDept)) ||
(this.isZQOrZZPowerPlatformDept(currentParam.department) && this.isZQOrZZPowerPlatformDept(responsiblePerson.department || ''))
if (!isAliasMatch) {
//
shouldDispatch = false
}
}
}
}
}
@ -3427,6 +3850,7 @@ export default {
if (dispatchData.length === 0) {
const fieldName = isUpdateParamMode ? '填写人' : '责任人'
this.dispatchLoading = false
this.$message({
type: 'warning',
message: `没有${fieldName}的数据需要分发`
@ -4537,7 +4961,13 @@ export default {
const paramData = {
parameterName: parameterName,
partsName: partsName,
parameterValue: item.parameterValue || item.value || '', //
parameterValue: item.parameterValue !== undefined && item.parameterValue !== null && item.parameterValue !== ''
? item.parameterValue
: (item.value !== undefined && item.value !== null && item.value !== ''
? item.value
: (item.minorVersion !== undefined && item.minorVersion !== null && item.minorVersion !== ''
? item.minorVersion
: '')),
parameterSource: item.parameterSource || '', // 使 GET_ENGINE_DETAIL
unit: item.unit || '-',
department: department,
@ -4558,6 +4988,8 @@ export default {
isDistributedFilled: item.isDistributedFilled || 0 // 10
}
const mergedParamData = this.applyDispatchParamDraft(paramData)
// filler filledByUserId fillerUserOptions
@ -5476,14 +5908,21 @@ export default {
// selectedIds id : ['id1', 'id2', ...]
// v-model step.handlers id
},
// /
// / /
isZQOrZZPowerPlatformDept(department) {
if (!department) {
return false
}
const normalized = String(department).replace(/·/g, '-')
const displayName = this.formatDepartmentDisplay(normalized)
const aliasNames = ['中轻', '中重动力平台']
//
const aliasNames = [
'中轻',
'中重动力平台',
'平台部',
'中轻动力平台部', //
'中重动力平台部' //
]
return aliasNames.some(
(alias) =>
displayName === alias ||

View File

@ -5,6 +5,36 @@
<div class="title">
责任人管理
</div>
<div class="search-section">
<el-input
v-model="searchForm.department"
placeholder="搜索责任部门"
clearable
style="width: 180px; margin-right: 10px;"
@input="handleSearch"
/>
<el-input
v-model="searchForm.subsystem"
placeholder="搜索子系统"
clearable
style="width: 180px; margin-right: 10px;"
@input="handleSearch"
/>
<el-input
v-model="searchForm.username"
placeholder="搜索责任人"
clearable
style="width: 180px; margin-right: 10px;"
@input="handleSearch"
/>
<el-input
v-model="searchForm.partsName"
placeholder="搜索状态代号"
clearable
style="width: 180px; margin-right: 10px;"
@input="handleSearch"
/>
</div>
<div v-if="hasEditPermission()">
<el-button type="primary" @click="handleAdd">
新增
@ -36,8 +66,7 @@
{{ formatDepartmentDisplay(scope.row.department) }}
</template>
</el-table-column>
<el-table-column prop="subsystem" label="子系统" align="center" />
<el-table-column prop="partsName" label="状态代号" align="center" />
<el-table-column prop="partsName" label="项目编号" align="center" />
<el-table-column prop="username" label="责任人" width="250" align="center" />
<el-table-column v-if="hasEditPermission()" label="操作" width="200" align="center">
<template slot-scope="scope">
@ -308,7 +337,15 @@ export default {
importFileList: [], //
selectedImportFile: null, //
importing: false, //
canViewData: false //
canViewData: false, //
//
searchForm: {
department: '',
subsystem: '',
username: '',
partsName: ''
},
allTableData: [] //
}
},
computed: {
@ -521,8 +558,8 @@ export default {
const request = {
...api.GET_MANAGER_DATA,
params: {
pageNumber: this.pagination.currentPage,
pageSize: this.pagination.pageSize
pageNumber: 1,
pageSize: 9999 //
},
disableSuccessMsg: true
}
@ -530,20 +567,72 @@ export default {
.asyncThen((resp) => {
//
if (resp && resp.data) {
this.tableData = resp.data.result
// total
this.pagination.total = resp.data.count
this.allTableData = resp.data.result || []
this.pagination.total = resp.data.count || this.allTableData.length
//
this.handleSearch()
} else {
this.allTableData = []
this.tableData = []
this.pagination.total = 0
}
})
.asyncErrorCatch((err) => {
console.error('加载管理数据失败:', err)
this.allTableData = []
this.tableData = []
this.pagination.total = 0
})
},
//
handleSearch() {
let filteredData = [...this.allTableData]
//
if (this.searchForm.department && this.searchForm.department.trim()) {
const deptKeyword = this.searchForm.department.trim().toLowerCase()
filteredData = filteredData.filter(item => {
const dept = this.formatDepartmentDisplay(item.department || '')
return dept.toLowerCase().includes(deptKeyword) ||
(item.department || '').toLowerCase().includes(deptKeyword)
})
}
//
if (this.searchForm.subsystem && this.searchForm.subsystem.trim()) {
const subsystemKeyword = this.searchForm.subsystem.trim().toLowerCase()
filteredData = filteredData.filter(item => {
const subsystem = item.subsystem || ''
return subsystem.toLowerCase().includes(subsystemKeyword)
})
}
//
if (this.searchForm.username && this.searchForm.username.trim()) {
const usernameKeyword = this.searchForm.username.trim().toLowerCase()
filteredData = filteredData.filter(item => {
const username = item.username || item.userName || ''
return username.toLowerCase().includes(usernameKeyword)
})
}
//
if (this.searchForm.partsName && this.searchForm.partsName.trim()) {
const partsNameKeyword = this.searchForm.partsName.trim().toLowerCase()
filteredData = filteredData.filter(item => {
const partsName = item.partsName || ''
return partsName.toLowerCase().includes(partsNameKeyword)
})
}
//
this.pagination.total = filteredData.length
//
const startIndex = (this.pagination.currentPage - 1) * this.pagination.pageSize
const endIndex = startIndex + this.pagination.pageSize
this.tableData = filteredData.slice(startIndex, endIndex)
},
loadDepartmentList() {
const request = {
...api.GET_DEPT_OF_USER,
@ -983,12 +1072,12 @@ export default {
handleSizeChange(val) {
this.pagination.pageSize = val
this.pagination.currentPage = 1 //
this.loadManagerData()
this.handleSearch() //
},
//
handleCurrentChange(val) {
this.pagination.currentPage = val
this.loadManagerData()
this.handleSearch() //
},
//
handleRefresh() {
@ -1335,6 +1424,7 @@ export default {
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.comment-text {
color: #909399;
@ -1344,11 +1434,15 @@ export default {
color: #303133;
font-weight: 700;
margin-left: 20px;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
font-size: 18px;
}
.search-section {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
justify-content: center;
}
}
.table-section {

View File

@ -104,6 +104,9 @@
<th class="param-column">
参数名称
</th>
<th class="unit-column">
单位
</th>
<th v-for="(model, index) in comparisonModels" :key="index" class="model-column">
<div class="model-header">
<div v-if="model.name" class="model-name"
@ -145,6 +148,9 @@
@click.stop="handleHideParam(param)"
></i>
</td>
<td class="unit-column">
{{ param.unit || '-' }}
</td>
<td
v-for="(model, modelIndex) in comparisonModels"
:key="modelIndex"
@ -490,7 +496,8 @@ export default {
subsystemName: subsystemName,
parameterId: parameterId,
partsName: partsName, // partsName
displayPartsName: displayPartsName //
displayPartsName: displayPartsName, //
unit: item.unit || '' //
})
}
}
@ -2481,7 +2488,8 @@ export default {
parameterId: param.parameterId || '',
parameterName: param.parameterName || '',
partsName: param.partsName || '',
subsystem: item.subsystem //
subsystem: item.subsystem, //
unit: param.unit || '' //
}))
}
})
@ -2858,6 +2866,13 @@ export default {
z-index: 15;
}
&.unit-column {
width: 80px;
min-width: 80px;
left: 460px;
z-index: 15;
}
&.model-column {
min-width: 180px;
text-align: center;
@ -2957,7 +2972,8 @@ export default {
td {
&.subsystem-column,
&.parts-column,
&.param-column {
&.param-column,
&.unit-column {
background: #f5f7fa;
}
}
@ -3007,6 +3023,14 @@ export default {
}
}
&.unit-column {
position: sticky;
left: 460px;
background: white;
z-index: 5;
text-align: center;
}
&.model-column {
text-align: center;
}

View File

@ -20,6 +20,9 @@
<el-button v-if="hasTemplateManagementEditPermission()" class="import-btn" @click="handleImportTemplate()">
导入参数模板
</el-button>
<el-button v-if="hasTemplateManagementEditPermission()" type="primary" @click="handleShowOrderDialog">
调整排序
</el-button>
<!-- <el-button v-if="hasTemplateManagementEditPermission()" type="primary" @click="handleAddTemplate()">
添加参数模板
</el-button> -->
@ -232,6 +235,37 @@
>
<div class="error-list"> {{ errorList }} </div>
</el-dialog>
<el-dialog
title="调整排序"
:visible.sync="orderDialogVisible"
width="600px"
append-to-body
@close="handleOrderDialogClose"
>
<div class="order-dialog-content">
<div class="order-tip" style="margin-bottom: 10px; color: #909399; font-size: 12px">
提示拖动行可调整排序拖动后排序号会自动更新
</div>
<el-table
:data="orderList"
row-key="id"
class="order-table"
max-height="600"
>
<el-table-column label="排序号" width="80" align="center">
<template slot-scope="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="模板名称" prop="templateName" />
</el-table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleOrderDialogClose">取消</el-button>
<el-button type="primary" :loading="updatingOrder" @click="handleUpdateOrder">确定</el-button>
</span>
</el-dialog>
</div>
</template>
@ -280,7 +314,11 @@ export default {
userId: getUserId(),
errorDialogVisible: false,
templateManagementPermissions: [], // menuPath "/"
fromTodo: false //
fromTodo: false, //
orderDialogVisible: false,
orderList: [],
updatingOrder: false,
dragSortHandler: null
}
},
computed: {
@ -1090,13 +1128,130 @@ export default {
this.templateSelectLoading = false
})
},
// UUID
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
},
handleShowOrderDialog() {
const request = {
...api.GET_MODEL_LIST,
params: {
pageNumber: 1,
pageSize: 999
},
disableSuccessMsg: true
}
this.$request(request, false)
.asyncThen((resp) => {
const list = resp.data.result || []
this.orderList = list.map((item, index) => ({
id: item.id || item.row_id || `temp_${index}_${Date.now()}`,
templateName: item.templateName || item.po_name,
orderNum: item.orderNum || item.order_num || (index + 1)
}))
this.orderDialogVisible = true
this.$nextTick(() => {
this.initDragSort()
})
})
.asyncErrorCatch((err) => {
console.error('加载模板列表失败:', err)
this.$message.error('加载模板列表失败')
})
},
handleOrderDialogClose() {
this.orderDialogVisible = false
this.orderList = []
},
initDragSort() {
const table = document.querySelector('.order-table .el-table__body-wrapper tbody')
if (!table) return
const rows = table.querySelectorAll('tr')
rows.forEach((row) => {
row.draggable = true
row.style.cursor = 'move'
})
if (this.dragSortHandler) {
table.removeEventListener('dragstart', this.dragSortHandler.dragstart)
table.removeEventListener('dragend', this.dragSortHandler.dragend)
table.removeEventListener('dragover', this.dragSortHandler.dragover)
table.removeEventListener('drop', this.dragSortHandler.drop)
}
let draggedIndex = null
const dragstartHandler = (e) => {
const row = e.target.closest('tr')
if (!row) return
const rows = Array.from(table.querySelectorAll('tr'))
draggedIndex = rows.indexOf(row)
row.style.opacity = '0.5'
e.dataTransfer.effectAllowed = 'move'
}
const dragendHandler = (e) => {
const row = e.target.closest('tr')
if (row) row.style.opacity = '1'
draggedIndex = null
}
const dragoverHandler = (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
}
const dropHandler = (e) => {
e.preventDefault()
e.stopPropagation()
const targetRow = e.target.closest('tr')
if (!targetRow || draggedIndex === null) return
const rows = Array.from(table.querySelectorAll('tr'))
const targetIndex = rows.indexOf(targetRow)
if (draggedIndex !== targetIndex) {
const newOrderList = [...this.orderList]
const draggedItem = newOrderList[draggedIndex]
newOrderList.splice(draggedIndex, 1)
newOrderList.splice(targetIndex, 0, draggedItem)
this.orderList = newOrderList
this.$nextTick(() => {
this.initDragSort()
})
}
}
table.addEventListener('dragstart', dragstartHandler)
table.addEventListener('dragend', dragendHandler)
table.addEventListener('dragover', dragoverHandler)
table.addEventListener('drop', dropHandler)
this.dragSortHandler = {
dragstart: dragstartHandler,
dragend: dragendHandler,
dragover: dragoverHandler,
drop: dropHandler
}
},
handleUpdateOrder() {
this.updatingOrder = true
const updateList = this.orderList.map((item, index) => ({
id: item.id,
orderNum: index + 1
}))
const request = {
...api.UPDATE_TEMPLATE_ORDER,
params: {
items: updateList
}
}
this.$request(request)
.asyncThen((resp) => {
this.$message.success('排序更新成功')
this.handleOrderDialogClose()
this.loadData()
})
.asyncErrorCatch((err) => {
console.error('更新排序失败:', err)
this.$message.error('更新排序失败')
})
.finally(() => {
this.updatingOrder = false
})
}
}
}
@ -1299,4 +1454,24 @@ export default {
::v-deep .el-button {
line-height: 0.85 !important;
}
.order-dialog-content {
.order-table {
width: 100%;
}
.order-tip {
padding: 8px 12px;
background: #f4f4f5;
border-radius: 4px;
}
::v-deep .el-table__body tr {
cursor: move;
transition: all 0.3s;
}
::v-deep .el-table__body tr:hover {
background-color: #f5f7fa;
}
::v-deep .el-table__body tr[draggable="true"] {
cursor: move;
}
}
</style>

View File

@ -944,6 +944,13 @@ export default {
width: '100',
align: 'center'
},
{
prop: 'parameterValue',
label: '参数值',
showOverflowTooltip: true,
minWidth: '120',
align: 'center'
},
{
prop: 'isNumber',
label: '数值类型',
@ -1049,6 +1056,13 @@ export default {
width: '100',
align: 'center'
},
{
prop: 'parameterValue',
label: '参数值',
showOverflowTooltip: true,
minWidth: '120',
align: 'center'
},
{
prop: 'isNumber',
label: '数值类型',
@ -1391,6 +1405,12 @@ export default {
if (this.detail && this.detail.flowId) {
this.getApprovalSpet()
}
//
if (this.fromTodo && this.flowId) {
this.$nextTick(() => {
this.loadData()
})
}
},
methods: {
//

View File

@ -1,11 +1,21 @@
<template>
<div class="permission-management-page custom-page">
<div class="page-header">
<div class="title">
权限管理
<!-- 人员管理组件 -->
<apaas-custom-roleMemberManagement
v-if="showRoleMemberManagement"
:role-id="currentRoleId"
:role-name="currentRoleName"
@back="handleBackFromRoleMember"
/>
<!-- 权限管理主页面 -->
<div v-if="!showRoleMemberManagement">
<div class="page-header">
<div class="title">
权限管理
</div>
</div>
</div>
<div class="page-content">
<div class="page-content">
<div class="header-section">
<el-button type="primary" @click="handleAddRole">
新增角色
@ -78,6 +88,7 @@
</div>
</div>
</div>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
@ -146,7 +157,7 @@
<el-dialog
title="人员配置"
:visible.sync="personConfigDialogVisible"
width="500px"
width="800px"
:close-on-click-modal="false"
:modal="false"
class="person-config-dialog"
@ -194,34 +205,28 @@
</template>
<!-- 配置人员模式 -->
<template v-else>
<!-- <el-form-item label="责任部门:" prop="department">
<el-cascader
ref="departmentCascader"
v-model="personConfigFormData.department"
:options="departmentList"
:props="departmentCascaderProps"
filterable
placeholder="请选择责任部门"
<el-form-item label="搜索人员:">
<el-input
v-model="personSearchKeyword"
placeholder="在已选人员中搜索人名"
clearable
style="width: 100%"
@change="handlePersonDepartmentChange"
@input="handlePersonSearchInList"
/>
</el-form-item> -->
<el-form-item label="人员:" prop="person">
</el-form-item>
<el-form-item label="添加人员:">
<el-select
v-model="personConfigFormData.person"
multiple
v-model="personToAdd"
filterable
remote
reserve-keyword
collapse-tags
:max-collapse-tags="3"
placeholder="请输入人员姓名进行搜索"
placeholder="请输入人员姓名进行搜索并添加"
style="width: 100%"
clearable
:remote-method="queryPersonByDepartment"
:loading="personSearchLoading"
class="person-select"
@change="handleAddPersonToList"
>
<el-option
v-for="person in personOptions"
@ -234,6 +239,32 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="已选人员:">
<el-table
:data="filteredPersonList"
border
max-height="300"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="username" label="姓名" align="center" />
<el-table-column prop="userNumber" label="ID" align="center" />
<el-table-column label="操作" width="80" align="center">
<template slot-scope="{ row }">
<el-button
type="text"
style="color: #f56c6c"
@click="handleRemovePersonFromList(row)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 8px; color: #909399; font-size: 12px">
{{ personConfigFormData.person.length }}
</div>
</el-form-item>
</template>
</el-form>
<div slot="footer" class="dialog-footer">
@ -348,7 +379,11 @@ export default {
roleDescription: '',
roleId: '' // 使
},
addRoleFormRules: {
//
showRoleMemberManagement: false,
currentRoleId: '',
currentRoleName: '',
addRoleFormRules: {
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
roleCode: [{ required: true, message: '请输入角色代码', trigger: 'blur' }],
roleDescription: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
@ -383,6 +418,9 @@ export default {
},
personOptions: [], //
personSearchLoading: false, // loading
personSearchKeyword: '', //
personToAdd: '', // ID
selectedPersonList: [], // [{ userId, username, userNumber }]
selectedDepartmentOptions: [], // { value, label }
personConfigFormRules: {
department: [
@ -433,6 +471,21 @@ export default {
}
},
computed: {
filteredPersonList() {
let list = []
if (!this.personSearchKeyword || this.personSearchKeyword.trim() === '') {
list = this.selectedPersonList
} else {
//
const keyword = this.personSearchKeyword.trim().toLowerCase()
list = this.selectedPersonList.filter((person) => {
const name = (person.username || '').toLowerCase()
const number = (person.userNumber || '').toLowerCase()
return name.includes(keyword) || number.includes(keyword)
})
}
return list.slice().reverse()
},
dialogTitle() {
return this.dialogMode === 'add' ? '新增' : '修改'
},
@ -1507,96 +1560,21 @@ export default {
//
})
},
//
// -
handlePersonConfig(row) {
this.personConfigFormData = {
configType: 'person',
department: '',
person: [], // userId
selectDeptId: '',
selectDeptName: '',
selectDeptIds: [],
roleId: row.rowId || row.id || '',
roleName: row.roleName || ''
}
this.selectedDepartmentOptions = []
const request = {
...api.CHECK_ROLE_USER_DEPT,
params: {
roleId: row.rowId || row.id || ''
},
disableSuccessMsg: true
}
this.$request(request)
.asyncThen((resp) => {
if (resp && resp.data) {
// { "": ["-ID"], "": ["-yc90117847"] }
const respData = resp.data
const personArray = respData['人员'] || respData.person || []
const departmentArray = respData['部门'] || respData.department || []
// ["-ID"] [{ deptId, deptName }]ID
if (departmentArray.length > 0) {
const deptIds = []
const optionsFromApi = []
departmentArray.forEach((item) => {
if (typeof item === 'string' && item.includes('-')) {
const parts = item.split('-')
if (parts.length >= 2) {
const deptName = parts[0]
const deptId = parts.slice(1).join('-')
const idStr = deptId == null ? '' : String(deptId)
deptIds.push(idStr)
optionsFromApi.push({ value: idStr, label: deptName || idStr })
}
} else if (item && typeof item === 'object' && (item.deptId != null || item.id != null)) {
const id = item.deptId != null ? item.deptId : item.id
const name = item.deptName || item.name || item.label || ''
const idStr = id == null ? '' : String(id)
deptIds.push(idStr)
optionsFromApi.push({ value: idStr, label: name || idStr })
}
})
this.personConfigFormData.selectDeptIds = deptIds
this.personConfigFormData.department = deptIds
this.selectedDepartmentOptions = optionsFromApi
}
// ["userName-userId"]
const userIds = []
const personOptionsMap = new Map()
personArray.forEach((personStr) => {
if (typeof personStr === 'string' && personStr.includes('-')) {
const parts = personStr.split('-')
if (parts.length >= 2) {
const userName = parts[0]
const userId = parts.slice(1).join('-') // userId -
userIds.push(userId)
//
personOptionsMap.set(userId, {
userId: userId,
username: userName,
userNumber: userId // userNumber 使 userId
})
}
}
})
// ID
this.personConfigFormData.person = userIds
//
this.personOptions = Array.from(personOptionsMap.values())
}
})
.asyncErrorCatch((err) => {
console.error('加载人员列表失败:', err)
})
this.personConfigDialogVisible = true
this.$nextTick(() => {
if (this.$refs.personConfigForm) {
this.$refs.personConfigForm.clearValidate()
}
})
console.log('打开人员管理页面:', row)
this.currentRoleId = row.rowId || row.id || ''
this.currentRoleName = row.roleName || ''
this.showRoleMemberManagement = true
},
//
handleBackFromRoleMember() {
console.log('从人员管理页面返回')
this.showRoleMemberManagement = false
this.currentRoleId = ''
this.currentRoleName = ''
},
//
handleConfigTypeChange(value) {
// department id selectDeptIds el-select
@ -1960,6 +1938,9 @@ export default {
roleName: ''
}
this.personOptions = []
this.personSearchKeyword = ''
this.personToAdd = ''
this.selectedPersonList = []
//
if (this.personSearchTimer) {
clearTimeout(this.personSearchTimer)
@ -1970,6 +1951,40 @@ export default {
this.$refs.personConfigForm.clearValidate()
}
},
handlePersonSearchInList() {
},
handleAddPersonToList(userId) {
if (!userId) {
return
}
const person = this.personOptions.find((p) => p.userId === userId)
if (!person) {
return
}
const exists = this.selectedPersonList.some((p) => p.userId === userId)
if (exists) {
this.$message.warning('该人员已在列表中')
this.personToAdd = ''
return
}
this.selectedPersonList.push({
userId: person.userId,
username: person.username,
userNumber: person.userNumber
})
this.personConfigFormData.person.push(userId)
this.personToAdd = ''
},
handleRemovePersonFromList(row) {
const index = this.selectedPersonList.findIndex((p) => p.userId === row.userId)
if (index > -1) {
this.selectedPersonList.splice(index, 1)
const idIndex = this.personConfigFormData.person.indexOf(row.userId)
if (idIndex > -1) {
this.personConfigFormData.person.splice(idIndex, 1)
}
}
},
//
handleEdit(row) {
this.addRoleMode = 'edit'

View File

@ -0,0 +1,520 @@
<template>
<div class="role-member-management-page custom-page">
<div class="page-header">
<div class="header-left">
<el-button
type="text"
icon="el-icon-arrow-left"
class="back-button"
@click="handleBack"
>
返回
</el-button>
<div class="title-info">
<span class="title">人员管理</span>
<span class="role-tip">当前选择角色<span class="role-name">{{ roleName }}</span></span>
</div>
</div>
</div>
<div class="page-content">
<!-- 搜索区域 -->
<div class="search-section">
<div class="search-form">
<div class="search-item">
<span class="label">成员名称</span>
<el-input
v-model="searchForm.memberName"
placeholder="请输入成员名称"
clearable
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="label">成员编码</span>
<el-input
v-model="searchForm.memberCode"
placeholder="请输入成员编码"
clearable
style="width: 200px"
/>
</div>
<div class="search-item">
<span class="label">部门</span>
<el-input
v-model="searchForm.department"
placeholder="请输入部门"
clearable
style="width: 200px"
/>
</div>
<div class="search-actions">
<el-button type="primary" @click="handleSearch">
查询
</el-button>
<el-button @click="handleReset">
重置
</el-button>
</div>
</div>
</div>
<!-- 操作区域 -->
<div class="action-section">
<el-button type="primary" @click="handleAddMember">
添加成员
</el-button>
<el-button :disabled="selectedMembers.length === 0" @click="handleDeleteMember">
删除成员
</el-button>
</div>
<!-- 表格区域 -->
<div class="table-section">
<el-table
ref="memberTable"
:data="tableData"
border
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="username" label="成员名称" min-width="120" align="center" />
<el-table-column prop="userNumber" label="成员编码" min-width="120" align="center" />
<el-table-column label="部门" min-width="200" align="center" show-overflow-tooltip>
<template slot-scope="{ row }">
{{ formatDepartment(row) }}
</template>
</el-table-column>
</el-table>
<div class="pagination-section">
<el-pagination
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
<!-- 添加成员弹窗 -->
<el-dialog
title="添加成员"
:visible.sync="addMemberDialogVisible"
width="600px"
:close-on-click-modal="false"
append-to-body
@close="handleAddMemberDialogClose"
>
<el-form ref="addMemberForm" :model="addMemberFormData" label-width="100px">
<el-form-item label="搜索人员:">
<el-select
v-model="addMemberFormData.selectedUsers"
filterable
remote
multiple
reserve-keyword
placeholder="请输入人员姓名进行搜索"
style="width: 100%"
:remote-method="queryPerson"
:loading="personSearchLoading"
>
<el-option
v-for="person in personOptions"
:key="person.userNumber"
:value="person.userId"
:label="person.username"
>
<span>{{ person.username }}</span>
<span style="float: right; color: #8492a6; font-size: 12px">{{ person.userNumber }}</span>
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleAddMemberDialogClose">
取消
</el-button>
<el-button type="primary" :loading="addMemberSubmitting" @click="handleAddMemberSubmit">
确定
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import api from '@/api'
export default {
name: 'RoleMemberManagement',
props: {
roleId: {
type: String,
default: ''
},
roleName: {
type: String,
default: ''
}
},
data() {
return {
searchForm: {
memberName: '',
memberCode: '',
department: ''
},
tableData: [],
selectedMembers: [],
pagination: {
currentPage: 1,
pageSize: 10,
total: 0
},
addMemberDialogVisible: false,
addMemberFormData: {
selectedUsers: []
},
personOptions: [],
personSearchLoading: false,
addMemberSubmitting: false
}
},
mounted() {
console.log('人员管理组件 mounted, roleId:', this.roleId, 'roleName:', this.roleName)
this.loadMemberList()
},
methods: {
// //
formatDepartment(row) {
const parts = []
if (row.username) parts.push(row.username)
if (row.userNumber) parts.push(row.userNumber)
if (row.department) parts.push(row.department)
return parts.join('/')
},
handleBack() {
this.$emit('back')
},
handleSearch() {
this.pagination.currentPage = 1
this.loadMemberList()
},
handleReset() {
this.searchForm = {
memberName: '',
memberCode: '',
department: ''
}
this.pagination.currentPage = 1
this.loadMemberList()
},
handleAddMember() {
this.addMemberDialogVisible = true
this.addMemberFormData.selectedUsers = []
this.personOptions = []
},
handleDeleteMember() {
if (this.selectedMembers.length === 0) {
this.$message.warning('请先选择要删除的成员')
return
}
this.$confirm(`确定要删除选中的 ${this.selectedMembers.length} 个成员吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteMembers()
}).catch(() => {})
},
handleSelectionChange(selection) {
this.selectedMembers = selection
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.currentPage = 1
this.loadMemberList()
},
handleCurrentChange(page) {
this.pagination.currentPage = page
this.loadMemberList()
},
loadMemberList() {
const request = {
...api.CHECK_ROLE_USER_DEPT,
params: {
roleId: this.roleId
},
disableSuccessMsg: true
}
this.$request(request)
.asyncThen((resp) => {
if (resp && resp.data) {
const respData = resp.data
const personArray = respData['人员'] || respData.person || []
//
// ["-ID:100771324098969075712-userNumber:yc90117846-department:"]
const members = []
personArray.forEach((personStr) => {
if (typeof personStr === 'string') {
// -
const nameMatch = personStr.match(/^([^-]+)/)
const username = nameMatch ? nameMatch[1] : ''
// ID
const idMatch = personStr.match(/ID:([^-]+)/)
const userId = idMatch ? idMatch[1] : ''
// userNumber
const userNumberMatch = personStr.match(/userNumber:([^-]+)/)
const userNumber = userNumberMatch ? userNumberMatch[1] : ''
// department
const deptMatch = personStr.match(/department:(.+)$/)
const department = deptMatch ? deptMatch[1] : ''
if (username && userId) {
members.push({
userId: userId,
username: username,
userNumber: userNumber, //
department: department //
})
}
}
})
//
let filteredMembers = members
if (this.searchForm.memberName) {
filteredMembers = filteredMembers.filter(m =>
m.username && m.username.includes(this.searchForm.memberName)
)
}
if (this.searchForm.memberCode) {
filteredMembers = filteredMembers.filter(m =>
m.userNumber && m.userNumber.includes(this.searchForm.memberCode)
)
}
if (this.searchForm.department) {
filteredMembers = filteredMembers.filter(m =>
m.department && m.department.includes(this.searchForm.department)
)
}
//
this.pagination.total = filteredMembers.length
const startIndex = (this.pagination.currentPage - 1) * this.pagination.pageSize
const endIndex = startIndex + this.pagination.pageSize
this.tableData = filteredMembers.slice(startIndex, endIndex)
} else {
this.tableData = []
this.pagination.total = 0
}
})
.asyncErrorCatch((err) => {
console.error('加载成员列表失败:', err)
this.$message.error('加载成员列表失败,请稍后重试')
this.tableData = []
this.pagination.total = 0
})
},
queryPerson(query) {
if (!query || query.trim() === '') {
this.personOptions = []
return
}
this.personSearchLoading = true
const request = {
...api.GET_ALLUSERS,
params: {
name: query.trim()
},
disableSuccessMsg: true
}
this.$request(request)
.asyncThen((resp) => {
const respData = resp && resp.data
let result = []
if (Array.isArray(respData)) {
result = respData
} else if (respData && Array.isArray(respData.result)) {
result = respData.result
} else if (respData && respData.data && Array.isArray(respData.data)) {
result = respData.data
}
this.personOptions = result.map((user) => ({
userId: user.userId || user.id,
username: user.username || user.name,
userNumber: user.userNumber || user.userId || user.id
})).filter((user) => user.userId && user.userNumber)
this.personSearchLoading = false
})
.asyncErrorCatch((err) => {
console.error('搜索人员失败:', err)
this.personOptions = []
this.personSearchLoading = false
})
},
handleAddMemberDialogClose() {
this.addMemberDialogVisible = false
this.addMemberFormData.selectedUsers = []
this.personOptions = []
},
handleAddMemberSubmit() {
if (!this.addMemberFormData.selectedUsers || this.addMemberFormData.selectedUsers.length === 0) {
this.$message.warning('请选择要添加的成员')
return
}
this.addMemberSubmitting = true
// 使
const request = {
...api.GET_ROLE_PERMISSION_BING,
data: {
roleID: this.roleId,
userIDs: this.addMemberFormData.selectedUsers
}
}
this.$request(request)
.asyncThen((resp) => {
this.$message.success('添加成员成功')
this.addMemberSubmitting = false
this.handleAddMemberDialogClose()
this.loadMemberList()
})
.asyncErrorCatch((err) => {
console.error('添加成员失败:', err)
this.$message.error('添加成员失败')
this.addMemberSubmitting = false
})
},
deleteMembers() {
const userIds = this.selectedMembers.map((member) => member.userId)
// 使
const request = {
...api.DELETE_ROLE_PERMISSION,
params: {
roleID: this.roleId,
userIDs: userIds.join(',')
}
}
this.$request(request)
.asyncThen((resp) => {
this.$message.success('删除成员成功')
this.loadMemberList()
})
.asyncErrorCatch((err) => {
console.error('删除成员失败:', err)
this.$message.error('删除成员失败')
})
}
}
}
</script>
<style lang="scss" scoped>
.role-member-management-page {
.page-header {
padding: 20px;
background: #fff;
border-bottom: 1px solid #ebeef5;
.header-left {
display: flex;
align-items: center;
gap: 20px;
.back-button {
font-size: 16px;
color: #409eff;
}
.title-info {
display: flex;
align-items: center;
gap: 20px;
.title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.role-tip {
font-size: 14px;
color: #606266;
.role-name {
color: #f56c6c;
font-weight: 600;
}
}
}
}
}
.page-content {
padding: 20px;
.search-section {
background: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
.search-form {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
.search-item {
display: flex;
align-items: center;
gap: 10px;
.label {
font-size: 14px;
color: #606266;
white-space: nowrap;
}
}
.search-actions {
display: flex;
gap: 10px;
}
}
}
.action-section {
margin-bottom: 20px;
}
.table-section {
background: #fff;
padding: 20px;
border-radius: 4px;
.pagination-section {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
}
}
</style>

View File

@ -215,7 +215,7 @@ export default {
formatter: this.formatDateTime
}
],
seqType: '',
seqType: 'normal',
seqConfig: {},
layout: 'total, prev, pager, next, jumper, sizes'
}

View File

@ -182,7 +182,8 @@ export default {
this.$message.info('下载导入模板功能')
},
handleRevert() {
this.$message.info('退回功能')
this.isEditable = true
this.$message.success('已进入编辑模式,可修改数据')
},
handleSave() {
this.$message.info('保存功能')

View File

@ -20,6 +20,7 @@ import ApaasCustomMyDone from './custom-page/myDone.vue'
import ApaasCustomMySend from './custom-page/mySend.vue'
import ApaasCustomCheckPersonPermission from './custom-page/checkPersonPermission.vue'
import ApaasCustomPermissionManagement from './custom-page/permissionManagement.vue'
import ApaasCustomRoleMemberManagement from './custom-page/roleMemberManagement.vue'
import ApaasCustomLayout from './custom-layout/custom-layout.vue'
import { customFormComponentList } from './custom-component/form-component'
import { widgetConfigList } from './custom-component/form-config'
@ -48,6 +49,7 @@ const install = function(Vue, opts) {
Vue.component('apaas-custom-mySend', ApaasCustomMySend)
Vue.component('apaas-custom-checkPersonPermission', ApaasCustomCheckPersonPermission)
Vue.component('apaas-custom-permissionManagement', ApaasCustomPermissionManagement)
Vue.component('apaas-custom-roleMemberManagement', ApaasCustomRoleMemberManagement)
Vue.component('apaas-custom-layout', ApaasCustomLayout)
// 安装表单部件
// 注册自开发表单组件

View File

@ -45,6 +45,7 @@ Vue.addInterceptorsRequest('REQUEST_PARAMS_INTERCEPTOR', (e) => {
const authStore = store.state.authModule
if (authStore.token) {
e.headers['xdaptoken'] = authStore.token
// e.headers['xdaptoken'] = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3ODExNTMxMTIsImlhdCI6MTc4MTE0NTkxMiwieGRhcHVzZXJpZCI6IjEwMDc3MTMyNDA5ODk2OTA3NTcxMiJ9.9OKIAqMyTsrQkBSFX3_KcduxVoSe-Tz0cqlKsyNQD1AziiLVGh2w0uK-qu0y6fz916o2Crw8mJWqyrea8oKpdg '
}
e.headers['xdaptenantid'] = process.env.VUE_APP_TENANT_ID

15502
yarn.lock

File diff suppressed because it is too large Load Diff

10
打包命令.txt Normal file
View File

@ -0,0 +1,10 @@
编译器终端 npx x-apaas-cli build apaas-custom-mydemo
打包失败是因为Node.js版本过高v24.16.0与webpack 4不兼容。需要设置OpenSSL legacy provider。
方案1降级Node.js版本推荐14
方案2替换node-sass为dart-sass
本地启动 npm run serve