君哥
1.恢复之前工艺路线模块,删除字段计件/计时字段,工资定额修改成计划工时,新增计划执行人员。工艺绑定,工艺路线恢复。仅需删除bom。
2.对于新增订单需流转协同办公进行审批,审批完成流转生产管控-生产订单
3.新增审批管理,规范管理所有节点审批人。
4.修改菜单栏样式bug
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 审æ¹ç®¡çé
ç½® |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢å®¡æ¹æµç¨é
ç½®èç¹å表 |
| | | export function getApproveProcessConfigNodeList(type) { |
| | | return request({ |
| | | url: '/approveProcessConfigNode/list', |
| | | method: 'get', |
| | | params: { type }, |
| | | }) |
| | | } |
| | | |
| | | // æ°å¢å®¡æ¹æµç¨é
ç½®èç¹ |
| | | export function addApproveProcessConfigNode(data) { |
| | | return request({ |
| | | url: '/approveProcessConfigNode/add', |
| | | method: 'post', |
| | | data: data, |
| | | }) |
| | | } |
| | |
| | | // cover some element-ui styles
|
| | |
|
| | | .el-breadcrumb__inner,
|
| | | .el-breadcrumb__inner a {
|
| | | font-weight: 400 !important;
|
| | | }
|
| | |
|
| | | .el-upload {
|
| | | input[type="file"] {
|
| | | display: none !important;
|
| | | }
|
| | | }
|
| | |
|
| | | .el-upload__input {
|
| | | display: none;
|
| | | }
|
| | |
|
| | | .cell {
|
| | | .el-tag {
|
| | | margin-right: 0px;
|
| | | }
|
| | | }
|
| | |
|
| | | .small-padding {
|
| | | .cell {
|
| | | padding-left: 5px;
|
| | | padding-right: 5px;
|
| | | }
|
| | | }
|
| | |
|
| | | .fixed-width {
|
| | | .el-button--mini {
|
| | | padding: 7px 10px;
|
| | | width: 60px;
|
| | | }
|
| | | }
|
| | |
|
| | | .status-col {
|
| | | .cell {
|
| | | padding: 0 10px;
|
| | | text-align: center;
|
| | |
|
| | | .el-tag {
|
| | | margin-right: 0px;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // to fixed https://github.com/ElemeFE/element/issues/2461
|
| | | // cover some element-ui styles |
| | | |
| | | .el-breadcrumb__inner, |
| | | .el-breadcrumb__inner a { |
| | | font-weight: 400 !important; |
| | | } |
| | | |
| | | .el-upload { |
| | | input[type="file"] { |
| | | display: none !important; |
| | | } |
| | | } |
| | | |
| | | .el-upload__input { |
| | | display: none; |
| | | } |
| | | |
| | | .cell { |
| | | .el-tag { |
| | | margin-right: 0px; |
| | | } |
| | | } |
| | | |
| | | .small-padding { |
| | | .cell { |
| | | padding-left: 5px; |
| | | padding-right: 5px; |
| | | } |
| | | } |
| | | |
| | | .fixed-width { |
| | | .el-button--mini { |
| | | padding: 7px 10px; |
| | | width: 60px; |
| | | } |
| | | } |
| | | |
| | | .status-col { |
| | | .cell { |
| | | padding: 0 10px; |
| | | text-align: center; |
| | | |
| | | .el-tag { |
| | | margin-right: 0px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // to fixed https://github.com/ElemeFE/element/issues/2461 |
| | | .el-dialog { |
| | | transform: none; |
| | | left: 0; |
| | |
| | | .el-message-box__content { |
| | | padding: 24px 24px 0; |
| | | } |
| | | .el-message-box__container {
|
| | | justify-content: center;
|
| | | }
|
| | | .el-message-box__btns {
|
| | | text-align: center;
|
| | | padding: 16px;
|
| | | display: flex;
|
| | | flex-direction: row-reverse;
|
| | | justify-content: center;
|
| | | align-items: center;
|
| | | .el-button--primary {
|
| | | margin-right: 12px;
|
| | | }
|
| | | }
|
| | | .el-message-box__container { |
| | | justify-content: center; |
| | | } |
| | | .el-message-box__btns { |
| | | text-align: center; |
| | | padding: 16px; |
| | | display: flex; |
| | | flex-direction: row-reverse; |
| | | justify-content: center; |
| | | align-items: center; |
| | | .el-button--primary { |
| | | margin-right: 12px; |
| | | } |
| | | } |
| | | .el-table__expanded-cell { |
| | | padding: 0 !important; |
| | | .el-table__header-wrapper { |
| | | background-color: var(--surface-soft) !important; |
| | | } |
| | | } |
| | |
|
| | | // refine element ui upload
|
| | | .upload-container {
|
| | | .el-upload {
|
| | | width: 100%;
|
| | |
|
| | | .el-upload-dragger {
|
| | | width: 100%;
|
| | | height: 200px;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // dropdown
|
| | | .el-dropdown-menu {
|
| | | a {
|
| | | display: block;
|
| | | }
|
| | | }
|
| | |
|
| | | // fix date-picker ui bug in filter-item
|
| | | .el-range-editor.el-input__inner {
|
| | | display: inline-flex !important;
|
| | | }
|
| | |
|
| | | // to fix el-date-picker css style
|
| | | .el-range-separator {
|
| | | box-sizing: content-box;
|
| | | }
|
| | |
|
| | | .el-menu--collapse
|
| | | > div
|
| | | > .el-submenu
|
| | | > .el-submenu__title
|
| | | .el-submenu__icon-arrow {
|
| | | display: none;
|
| | | }
|
| | |
|
| | | |
| | | // refine element ui upload |
| | | .upload-container { |
| | | .el-upload { |
| | | width: 100%; |
| | | |
| | | .el-upload-dragger { |
| | | width: 100%; |
| | | height: 200px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // dropdown |
| | | .el-dropdown-menu { |
| | | a { |
| | | display: block; |
| | | } |
| | | } |
| | | |
| | | // fix date-picker ui bug in filter-item |
| | | .el-range-editor.el-input__inner { |
| | | display: inline-flex !important; |
| | | } |
| | | |
| | | // to fix el-date-picker css style |
| | | .el-range-separator { |
| | | box-sizing: content-box; |
| | | } |
| | | |
| | | .el-menu--collapse |
| | | > div |
| | | > .el-sub-menu |
| | | > .el-sub-menu__title |
| | | .el-sub-menu__icon-arrow { |
| | | display: none; |
| | | } |
| | | |
| | | /* ç¡®ä¿èåç®å¤´å§ç»æ¾ç¤º - ä½¿ç¨ flex å¸å± */ |
| | | .el-sub-menu__title { |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | line-height: normal !important; |
| | | } |
| | | |
| | | .el-sub-menu__title .el-sub-menu__icon-arrow { |
| | | position: static !important; |
| | | display: inline-flex !important; |
| | | visibility: visible !important; |
| | | width: auto !important; |
| | | height: auto !important; |
| | | overflow: visible !important; |
| | | margin-left: auto !important; |
| | | align-self: center !important; |
| | | margin-top: 0 !important; |
| | | margin-bottom: 0 !important; |
| | | transform: none !important; |
| | | top: auto !important; |
| | | } |
| | | |
| | | .el-dropdown .el-dropdown-link { |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | |
| | | #app {
|
| | | .main-container {
|
| | | min-height: 100%;
|
| | | transition: margin-left 0.28s;
|
| | | margin-left: $base-sidebar-width;
|
| | | position: relative;
|
| | | background: transparent;
|
| | | }
|
| | |
|
| | | .sidebarHide {
|
| | | margin-left: 0 !important;
|
| | | }
|
| | |
|
| | | .sidebar-container {
|
| | | transition: width 0.28s;
|
| | | width: $base-sidebar-width !important;
|
| | | height: 100%;
|
| | | position: fixed;
|
| | | font-size: 0px;
|
| | | top: 0;
|
| | | bottom: 0;
|
| | | left: 0;
|
| | | z-index: 1001;
|
| | | overflow: hidden;
|
| | | padding: 12px 0 16px 16px;
|
| | | background: transparent;
|
| | | box-shadow: none;
|
| | |
|
| | | // reset element-ui css
|
| | | .horizontal-collapse-transition {
|
| | | transition: 0s width ease-in-out, 0s padding-left ease-in-out,
|
| | | 0s padding-right ease-in-out;
|
| | | }
|
| | |
|
| | | .scrollbar-wrapper {
|
| | | overflow-x: hidden !important;
|
| | | }
|
| | |
|
| | | .el-scrollbar__bar.is-vertical {
|
| | | right: 0px;
|
| | | }
|
| | |
|
| | | .el-scrollbar {
|
| | | height: 100%;
|
| | | }
|
| | |
|
| | | &.has-logo {
|
| | | .el-scrollbar {
|
| | | height: calc(100% - 72px);
|
| | | margin-top: 10px;
|
| | | }
|
| | | }
|
| | |
|
| | | .is-horizontal {
|
| | | display: none;
|
| | | }
|
| | |
|
| | | a {
|
| | | display: inline-block;
|
| | | width: 100%;
|
| | | overflow: hidden;
|
| | | }
|
| | |
|
| | | .svg-icon {
|
| | | margin-right: 16px;
|
| | | }
|
| | |
|
| | | #app { |
| | | .main-container { |
| | | min-height: 100%; |
| | | transition: margin-left 0.28s; |
| | | margin-left: $base-sidebar-width; |
| | | position: relative; |
| | | background: transparent; |
| | | } |
| | | |
| | | .sidebarHide { |
| | | margin-left: 0 !important; |
| | | } |
| | | |
| | | .sidebar-container { |
| | | transition: width 0.28s; |
| | | width: $base-sidebar-width !important; |
| | | height: 100%; |
| | | position: fixed; |
| | | top: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | z-index: 1001; |
| | | overflow: hidden; |
| | | padding: 12px 0 16px 16px; |
| | | background: transparent; |
| | | box-shadow: none; |
| | | |
| | | // reset element-ui css |
| | | .horizontal-collapse-transition { |
| | | transition: 0s width ease-in-out, 0s padding-left ease-in-out, |
| | | 0s padding-right ease-in-out; |
| | | } |
| | | |
| | | .scrollbar-wrapper { |
| | | overflow-x: hidden !important; |
| | | } |
| | | |
| | | .el-scrollbar__bar.is-vertical { |
| | | right: 0px; |
| | | } |
| | | |
| | | .el-scrollbar { |
| | | height: 100%; |
| | | } |
| | | |
| | | &.has-logo { |
| | | .el-scrollbar { |
| | | height: calc(100% - 72px); |
| | | margin-top: 10px; |
| | | } |
| | | } |
| | | |
| | | .is-horizontal { |
| | | display: none; |
| | | } |
| | | |
| | | a { |
| | | display: inline-block; |
| | | width: 100%; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .svg-icon { |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .el-menu { |
| | | border: none; |
| | | height: 100%; |
| | |
| | | backdrop-filter: blur(18px); |
| | | box-shadow: var(--shadow-sm); |
| | | } |
| | |
|
| | | .el-menu-item,
|
| | | .menu-title {
|
| | | overflow: hidden !important;
|
| | | text-overflow: ellipsis !important;
|
| | | white-space: nowrap !important;
|
| | | }
|
| | |
|
| | | .el-menu-item .el-menu-tooltip__trigger {
|
| | | display: inline-block !important;
|
| | | }
|
| | |
|
| | | |
| | | .el-menu-item, |
| | | .menu-title { |
| | | overflow: hidden !important; |
| | | text-overflow: ellipsis !important; |
| | | white-space: nowrap !important; |
| | | } |
| | | |
| | | .el-menu-item .el-menu-tooltip__trigger { |
| | | display: inline-block !important; |
| | | } |
| | | |
| | | // menu hover |
| | | .submenu-title-noDropdown, |
| | | .el-sub-menu__title { |
| | |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | | & .theme-light .is-active > .el-sub-menu__title {
|
| | | color: var(--current-color) !important;
|
| | | }
|
| | |
|
| | | |
| | | // ææåèåæ é¢ï¼ä½¿ç¨ flex å¸å±è®©ç®å¤´åæåå¨ä¸æ |
| | | .el-sub-menu__title { |
| | | padding-right: 10px !important; |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | line-height: normal !important; |
| | | } |
| | | |
| | | // 顶级åèåæ é¢ |
| | | & > .el-menu > .el-sub-menu > .el-sub-menu__title { |
| | | padding-right: 10px !important; |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | line-height: normal !important; |
| | | } |
| | | & .theme-light .is-active > .el-sub-menu__title { |
| | | color: var(--current-color) !important; |
| | | } |
| | | |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | min-width: 0 !important; |
| | | margin: 0 12px 6px; |
| | | width: calc(100% - 24px); |
| | | padding-left: 8px !important; |
| | | padding-right: 8px !important; |
| | | padding-right: 24px !important; |
| | | box-sizing: border-box; |
| | | |
| | | &:hover { |
| | |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | |
|
| | | |
| | | & .theme-light .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .theme-light .el-sub-menu .el-menu-item { |
| | | //background-color: transparent; |
| | |
| | | } |
| | | } |
| | | } |
| | |
|
| | | |
| | | .hideSidebar { |
| | | .sidebar-container { |
| | | width: 68px !important; |
| | |
| | | .main-container { |
| | | margin-left: 84px; |
| | | } |
| | |
|
| | | |
| | | .submenu-title-noDropdown { |
| | | padding: 0 !important; |
| | | position: relative; |
| | |
| | | width: 0; |
| | | overflow: hidden; |
| | | visibility: hidden; |
| | | display: inline-block;
|
| | | }
|
| | | & > i {
|
| | | height: 0;
|
| | | width: 0;
|
| | | overflow: hidden;
|
| | | visibility: hidden;
|
| | | display: inline-block;
|
| | | display: inline-block; |
| | | } |
| | | & > i.el-sub-menu__icon-arrow { |
| | | display: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
|
| | | .el-menu--collapse .el-menu .el-sub-menu {
|
| | | min-width: $base-sidebar-width !important;
|
| | | }
|
| | |
|
| | | // mobile responsive
|
| | | .mobile {
|
| | | .main-container {
|
| | | margin-left: 0px;
|
| | | }
|
| | |
|
| | | .sidebar-container {
|
| | | transition: transform 0.28s;
|
| | | width: $base-sidebar-width !important;
|
| | | }
|
| | |
|
| | | &.hideSidebar {
|
| | | .sidebar-container {
|
| | | pointer-events: none;
|
| | | transition-duration: 0.3s;
|
| | | transform: translate3d(-$base-sidebar-width, 0, 0);
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | .withoutAnimation {
|
| | | .main-container,
|
| | | .sidebar-container {
|
| | | transition: none;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // when menu collapsed
|
| | | .el-menu--vertical {
|
| | | & > .el-menu {
|
| | | .svg-icon {
|
| | | margin-right: 16px;
|
| | | }
|
| | | }
|
| | |
|
| | | |
| | | .el-menu--collapse .el-menu .el-sub-menu { |
| | | min-width: $base-sidebar-width !important; |
| | | } |
| | | |
| | | // mobile responsive |
| | | .mobile { |
| | | .main-container { |
| | | margin-left: 0px; |
| | | } |
| | | |
| | | .sidebar-container { |
| | | transition: transform 0.28s; |
| | | width: $base-sidebar-width !important; |
| | | } |
| | | |
| | | &.hideSidebar { |
| | | .sidebar-container { |
| | | pointer-events: none; |
| | | transition-duration: 0.3s; |
| | | transform: translate3d(-$base-sidebar-width, 0, 0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .withoutAnimation { |
| | | .main-container, |
| | | .sidebar-container { |
| | | transition: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // when menu collapsed |
| | | .el-menu--vertical { |
| | | & > .el-menu { |
| | | .svg-icon { |
| | | margin-right: 16px; |
| | | } |
| | | } |
| | | |
| | | .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | .el-menu-item { |
| | | min-width: 0 !important; |
| | |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | |
|
| | | // the scroll bar appears when the sub-menu is too long
|
| | | > .el-menu--popup {
|
| | | max-height: 100vh;
|
| | | overflow-y: auto;
|
| | | padding: 8px;
|
| | | border-radius: 18px;
|
| | | border: 1px solid var(--surface-border);
|
| | | box-shadow: var(--shadow-md);
|
| | |
|
| | | &::-webkit-scrollbar-track-piece {
|
| | | background: #dfe7e1;
|
| | | }
|
| | |
|
| | | &::-webkit-scrollbar {
|
| | | width: 6px;
|
| | | }
|
| | |
|
| | | &::-webkit-scrollbar-thumb {
|
| | | background: #9aa79e;
|
| | | border-radius: 20px;
|
| | | }
|
| | | }
|
| | | }
|
| | | |
| | | // the scroll bar appears when the sub-menu is too long |
| | | > .el-menu--popup { |
| | | max-height: 100vh; |
| | | overflow-y: auto; |
| | | padding: 8px; |
| | | border-radius: 18px; |
| | | border: 1px solid var(--surface-border); |
| | | box-shadow: var(--shadow-md); |
| | | |
| | | &::-webkit-scrollbar-track-piece { |
| | | background: #dfe7e1; |
| | | } |
| | | |
| | | &::-webkit-scrollbar { |
| | | width: 6px; |
| | | } |
| | | |
| | | &::-webkit-scrollbar-thumb { |
| | | background: #9aa79e; |
| | | border-radius: 20px; |
| | | } |
| | | } |
| | | } |
| | |
| | | </app-link>
|
| | | </template>
|
| | |
|
| | | <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
|
| | | <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)">
|
| | | <template v-if="item.meta" #title>
|
| | | <svg-icon :icon-class="item.meta && item.meta.icon" />
|
| | | <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
| | |
| | | |
| | | .el-sub-menu__title { |
| | | color: v-bind(getMenuTextColor); |
| | | padding-right: 10px !important; |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | line-height: normal !important; |
| | | } |
| | | |
| | | :deep(.el-sub-menu.is-active > .el-sub-menu__title) { |
| | |
| | | padding-left: 10px !important; |
| | | padding-right: 10px !important; |
| | | box-sizing: border-box; |
| | | overflow: hidden; |
| | | background-clip: padding-box; |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | line-height: normal !important; |
| | | } |
| | | |
| | | :deep(.el-menu-item.is-active) { |
| | |
| | | :deep(.el-menu-item:hover) { |
| | | border-radius: 14px; |
| | | } |
| | | |
| | | /* ç¡®ä¿åèåç®å¤´æ¾ç¤º - ä½¿ç¨ flex å¸å±è®©ç®å¤´åæåå¨ä¸æ */ |
| | | :deep(.el-sub-menu .el-sub-menu__title) { |
| | | display: flex !important; |
| | | align-items: center !important; |
| | | justify-content: flex-start !important; |
| | | line-height: normal !important; |
| | | } |
| | | |
| | | :deep(.el-sub-menu .el-sub-menu__title .el-sub-menu__icon-arrow) { |
| | | position: static !important; |
| | | display: inline-flex !important; |
| | | visibility: visible !important; |
| | | width: auto !important; |
| | | height: auto !important; |
| | | overflow: visible !important; |
| | | margin-left: auto !important; |
| | | margin-right: 0 !important; |
| | | order: 999 !important; |
| | | align-self: center !important; |
| | | margin-top: 0 !important; |
| | | margin-bottom: 0 !important; |
| | | transform: none !important; |
| | | top: auto !important; |
| | | } |
| | | |
| | | /* ç¡®ä¿ä¸çº§èåç®å¤´æ¾ç¤º */ |
| | | :deep(> .el-menu > .el-sub-menu > .el-sub-menu__title .el-sub-menu__icon-arrow) { |
| | | display: inline-flex !important; |
| | | visibility: visible !important; |
| | | } |
| | | |
| | | /* ç¡®ä¿äºçº§èåç®å¤´æ¾ç¤º */ |
| | | :deep(.nest-menu .el-sub-menu > .el-sub-menu__title .el-sub-menu__icon-arrow) { |
| | | display: inline-flex !important; |
| | | visibility: visible !important; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 页颿 é¢ --> |
| | | <div class="page-header"> |
| | | <div class="header-title"> |
| | | <el-icon class="title-icon"><Setting /></el-icon> |
| | | <span>å®¡æ¹æµç¨é
ç½®</span> |
| | | </div> |
| | | <div class="header-desc">为ä¸å审æ¹ç±»åé
ç½®å®¡æ¹æµç¨å审æ¹äºº</div> |
| | | </div> |
| | | |
| | | <!-- 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ --> |
| | | <div class="type-tabs"> |
| | | <div |
| | | v-for="type in approveTypes" |
| | | :key="type.value" |
| | | class="type-tab" |
| | | :class="{ active: activeTab === type.value }" |
| | | @click="activeTab = type.value; handleTabChange()" |
| | | > |
| | | <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }"> |
| | | <component :is="type.icon" /> |
| | | </el-icon> |
| | | <span class="tab-name">{{ type.label }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主è¦å
容åºå --> |
| | | <el-card class="config-card" shadow="hover" v-loading="loading"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <div class="header-left"> |
| | | <div class="type-icon" :style="{ backgroundColor: getTypeColor(currentApproveType) }"> |
| | | <el-icon :size="20" color="#fff"><component :is="getTypeIcon(currentApproveType)" /></el-icon> |
| | | </div> |
| | | <div class="header-info"> |
| | | <span class="type-name">{{ currentApproveTypeName }}</span> |
| | | <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light"> |
| | | {{ approverList.length > 0 ? `å·²é
ç½® ${approverList.length} 个审æ¹äºº` : 'æªé
置审æ¹äºº' }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="header-actions" v-if="approverList.length > 0"> |
| | | <el-button @click="handleReset" size="default"> |
| | | <el-icon><RefreshLeft /></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | <el-button type="primary" @click="handleSave" :loading="saveLoading" size="default"> |
| | | <el-icon><Check /></el-icon> |
| | | ä¿åé
ç½® |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- å®¡æ¹æµç¨å±ç¤º --> |
| | | <div class="flow-wrapper" v-if="approverList.length > 0"> |
| | | <div class="flow-container"> |
| | | <div |
| | | v-for="(item, index) in approverList" |
| | | :key="index" |
| | | class="flow-item" |
| | | > |
| | | <!-- 审æ¹èç¹å¡ç --> |
| | | <div class="node-card" :class="{ 'empty': !item.approverId }"> |
| | | <!-- é¡¶é¨åºå·åçº§å« --> |
| | | <div class="node-badge">{{ index + 1 }}</div> |
| | | |
| | | <!-- 头ååºå --> |
| | | <div class="node-avatar-section"> |
| | | <div |
| | | class="node-avatar" |
| | | :class="{ 'has-user': item.approverId }" |
| | | :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}" |
| | | > |
| | | <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span> |
| | | <el-icon v-else :size="24"><User /></el-icon> |
| | | </div> |
| | | <div class="node-level">{{ getLevelText(index) }}</div> |
| | | </div> |
| | | |
| | | <!-- éæ©åºå --> |
| | | <div class="node-select-section"> |
| | | <el-select |
| | | v-model="item.approverId" |
| | | placeholder="鿩审æ¹äºº" |
| | | filterable |
| | | size="default" |
| | | @change="(val) => handleApproverChange(val, item)" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <!-- æä½æé® --> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | :disabled="index === 0" |
| | | @click="moveLeft(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | title="åç§»" |
| | | > |
| | | <el-icon><ArrowLeft /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | :disabled="index === approverList.length - 1" |
| | | @click="moveRight(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | title="åç§»" |
| | | > |
| | | <el-icon><ArrowRight /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | circle |
| | | @click="handleDelete(index)" |
| | | size="small" |
| | | class="action-btn" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¿æ¥ç®å¤´ --> |
| | | <div class="arrow-connector" v-if="index < approverList.length - 1"> |
| | | <div class="arrow-line"></div> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ°å¢èç¹æé® - æ¾å¨æµç¨æå --> |
| | | <div class="add-node-item"> |
| | | <div class="arrow-connector" v-if="approverList.length > 0"> |
| | | <div class="arrow-line"></div> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <div class="add-node-card" @click="handleAdd"> |
| | | <div class="add-icon-wrapper"> |
| | | <el-icon :size="28"><Plus /></el-icon> |
| | | </div> |
| | | <span class="add-text">æ°å¢å®¡æ¹äºº</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <div class="empty-state" v-else> |
| | | <div class="empty-content"> |
| | | <div class="empty-icon-wrapper"> |
| | | <el-icon :size="48" color="#c0c4cc"><User /></el-icon> |
| | | </div> |
| | | <div class="empty-text">ææ å®¡æ¹äººé
ç½®</div> |
| | | <div class="empty-subtext">ç¹å»ä¸æ¹æé®æ·»å 第ä¸ä¸ªå®¡æ¹äºº</div> |
| | | <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢å®¡æ¹äºº |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- åºé¨æç¤º --> |
| | | <div class="bottom-tips"> |
| | | <el-icon><InfoFilled /></el-icon> |
| | | <span>æç¤ºï¼æ¯ä¸ªæµç¨è³å°é
ç½®ä¸ä¸ªå®¡æ¹äººï¼å®¡æ¹æé¡ºåºæµè½¬ï¼å¯éè¿ç®å¤´è°æ´é¡ºåº</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { |
| | | Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting, |
| | | Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked, |
| | | Van, ArrowRight, User, InfoFilled, Sell |
| | | } from '@element-plus/icons-vue'; |
| | | import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement'; |
| | | import { userListNoPage } from '@/api/system/user'; |
| | | |
| | | // å½åéä¸çæ ç¾é¡µ |
| | | const activeTab = ref('1'); |
| | | |
| | | // 审æ¹ç±»åé
ç½®æ°ç» |
| | | const approveTypes = [ |
| | | { value: '1', label: 'å
¬åºç®¡ç', icon: 'Suitcase', color: '#409EFF' }, |
| | | { value: '2', label: '请å管ç', icon: 'Calendar', color: '#67C23A' }, |
| | | { value: '3', label: 'åºå·®ç®¡ç', icon: 'Location', color: '#E6A23C' }, |
| | | { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | { value: '10', label: 'éå®å®¡æ¹', icon: 'Sell', color: '#FF6B6B' }, |
| | | ]; |
| | | |
| | | // 审æ¹ç±»ååç§°æ å° |
| | | const approveTypeNameMap = { |
| | | 1: 'å
¬åºç®¡ç', |
| | | 2: '请å管ç', |
| | | 3: 'åºå·®ç®¡ç', |
| | | 4: 'æ¥é管ç', |
| | | 5: 'éè´å®¡æ¹', |
| | | 6: 'æ¥ä»·å®¡æ¹', |
| | | 7: 'å货审æ¹', |
| | | 10: 'éå®å®¡æ¹', |
| | | }; |
| | | |
| | | // 审æ¹ç±»å徿 æ å° |
| | | const typeIconMap = { |
| | | 1: 'Suitcase', |
| | | 2: 'Calendar', |
| | | 3: 'Location', |
| | | 4: 'Money', |
| | | 5: 'ShoppingCart', |
| | | 6: 'DocumentChecked', |
| | | 7: 'Van', |
| | | 10: 'Sell', |
| | | }; |
| | | |
| | | // 审æ¹ç±»åé¢è²æ å° |
| | | const typeColorMap = { |
| | | 1: '#409EFF', |
| | | 2: '#67C23A', |
| | | 3: '#E6A23C', |
| | | 4: '#F56C6C', |
| | | 5: '#909399', |
| | | 6: '#9B59B6', |
| | | 7: '#1ABC9C', |
| | | 10: '#FF6B6B', |
| | | }; |
| | | |
| | | // 头åé¢è²æ± |
| | | const avatarColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#9B59B6', '#1ABC9C', '#FF6B6B', '#4ECDC4']; |
| | | |
| | | // å½å审æ¹ç±»ååç§° |
| | | const currentApproveTypeName = computed(() => { |
| | | return approveTypeNameMap[activeTab.value] || 'æªç¥ç±»å'; |
| | | }); |
| | | |
| | | // å½å审æ¹ç±»å |
| | | const currentApproveType = computed(() => { |
| | | return Number(activeTab.value); |
| | | }); |
| | | |
| | | // è·åç±»å徿 |
| | | const getTypeIcon = (type) => typeIconMap[type] || 'Setting'; |
| | | |
| | | // è·åç±»åé¢è² |
| | | const getTypeColor = (type) => typeColorMap[type] || '#409EFF'; |
| | | |
| | | // è·å头åé¢è² |
| | | const getAvatarColor = (name) => { |
| | | if (!name) return '#C0C4CC'; |
| | | let hash = 0; |
| | | for (let i = 0; i < name.length; i++) { |
| | | hash = name.charCodeAt(i) + ((hash << 5) - hash); |
| | | } |
| | | return avatarColors[Math.abs(hash) % avatarColors.length]; |
| | | }; |
| | | |
| | | // è·åçº§å«ææ¬ |
| | | const getLevelText = (index) => { |
| | | const texts = ['第ä¸çº§', '第äºçº§', '第ä¸çº§', '第å级', '第äºçº§', '第å
级', '第ä¸çº§', '第å
«çº§']; |
| | | return texts[index] || `第${index + 1}级`; |
| | | }; |
| | | |
| | | // 审æ¹äººå表ï¼ç宿¥å£ï¼ |
| | | const userList = ref([]); |
| | | |
| | | // 审æ¹äººå表 |
| | | const approverList = ref([]); |
| | | |
| | | // åå§æ°æ®ï¼ç¨äºéç½® |
| | | const originalList = ref([]); |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false); |
| | | const saveLoading = ref(false); |
| | | |
| | | // æ ç¾é¡µåæ¢å¤ç |
| | | const handleTabChange = () => { |
| | | loadData(); |
| | | }; |
| | | |
| | | // å 载审æ¹é
ç½®æ°æ®ï¼æ¨¡æï¼ |
| | | const loadData = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const res = await getApproveProcessConfigNodeList(currentApproveType.value); |
| | | const source = Array.isArray(res?.data) |
| | | ? res.data |
| | | : Array.isArray(res?.rows) |
| | | ? res.rows |
| | | : Array.isArray(res?.data?.records) |
| | | ? res.data.records |
| | | : []; |
| | | const data = source.map((item, index) => ({ |
| | | ...item, |
| | | sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1, |
| | | })); |
| | | approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); |
| | | originalList.value = JSON.parse(JSON.stringify(approverList.value)); |
| | | } catch (error) { |
| | | approverList.value = []; |
| | | originalList.value = []; |
| | | ElMessage.error('å 载审æ¹é
置失败'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const loadUserList = async () => { |
| | | try { |
| | | const res = await userListNoPage(); |
| | | userList.value = Array.isArray(res?.data) ? res.data : []; |
| | | } catch (error) { |
| | | userList.value = []; |
| | | ElMessage.error('å 载人åå表失败'); |
| | | } |
| | | }; |
| | | |
| | | // 审æ¹äººéæ©åå |
| | | const handleApproverChange = (userId, row) => { |
| | | const user = userList.value.find((u) => u.userId === userId); |
| | | if (user) { |
| | | row.approverName = user.nickName; |
| | | } |
| | | }; |
| | | |
| | | // æ°å¢å®¡æ¹äºº |
| | | const handleAdd = () => { |
| | | const newOrder = approverList.value.length + 1; |
| | | approverList.value.push({ |
| | | id: null, |
| | | approveType: currentApproveType.value, |
| | | approverId: null, |
| | | approverName: '', |
| | | sortOrder: newOrder, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤å®¡æ¹äºº |
| | | const handleDelete = (index) => { |
| | | ElMessageBox.confirm('ç¡®å®å é¤è¯¥å®¡æ¹äººåï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning', |
| | | }) |
| | | .then(() => { |
| | | approverList.value.splice(index, 1); |
| | | approverList.value.forEach((item, idx) => { |
| | | item.sortOrder = idx + 1; |
| | | }); |
| | | ElMessage.success('å 餿å'); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // åç§» |
| | | const moveLeft = (index) => { |
| | | if (index === 0) return; |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index - 1]; |
| | | approverList.value[index - 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index - 1].sortOrder = index; |
| | | }; |
| | | |
| | | // åç§» |
| | | const moveRight = (index) => { |
| | | if (index === approverList.value.length - 1) return; |
| | | const temp = approverList.value[index]; |
| | | approverList.value[index] = approverList.value[index + 1]; |
| | | approverList.value[index + 1] = temp; |
| | | approverList.value[index].sortOrder = index + 1; |
| | | approverList.value[index + 1].sortOrder = index + 2; |
| | | }; |
| | | |
| | | // ä¿åé
ç½® |
| | | const handleSave = async () => { |
| | | if (approverList.value.length === 0) { |
| | | ElMessage.warning('请è³å°é
ç½®ä¸ä¸ªå®¡æ¹äºº'); |
| | | return; |
| | | } |
| | | |
| | | const hasEmptyApprover = approverList.value.some((item) => !item.approverId); |
| | | if (hasEmptyApprover) { |
| | | ElMessage.warning('è¯·éæ©ææå®¡æ¹äºº'); |
| | | return; |
| | | } |
| | | |
| | | const approverIds = approverList.value.map((item) => item.approverId); |
| | | const uniqueIds = [...new Set(approverIds)]; |
| | | if (uniqueIds.length !== approverIds.length) { |
| | | ElMessage.warning('审æ¹äººä¸è½éå¤'); |
| | | return; |
| | | } |
| | | |
| | | saveLoading.value = true; |
| | | try { |
| | | const payload = approverList.value.map((item, index) => ({ |
| | | approveType: currentApproveType.value, |
| | | nodeOrder: index + 1, |
| | | approverId: item.approverId, |
| | | approverName: item.approverName, |
| | | })); |
| | | await addApproveProcessConfigNode(payload); |
| | | ElMessage.success('ä¿åæå'); |
| | | await loadData(); |
| | | } catch (error) { |
| | | ElMessage.error('ä¿å失败'); |
| | | } finally { |
| | | saveLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // éç½® |
| | | const handleReset = () => { |
| | | if (originalList.value.length === 0) { |
| | | approverList.value = []; |
| | | return; |
| | | } |
| | | ElMessageBox.confirm('ç¡®å®è¦éç½®å½åé
ç½®åï¼æªä¿åçæ´æ¹å°ä¸¢å¤±ã', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning', |
| | | }) |
| | | .then(() => { |
| | | approverList.value = JSON.parse(JSON.stringify(originalList.value)); |
| | | ElMessage.success('å·²éç½®'); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | await loadUserList(); |
| | | await loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .page-header { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .header-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .title-icon { |
| | | font-size: 24px; |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .header-desc { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | margin-left: 34px; |
| | | } |
| | | |
| | | /* 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ */ |
| | | .type-tabs { |
| | | display: flex; |
| | | gap: 4px; |
| | | margin-bottom: 16px; |
| | | padding: 4px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | .type-tab { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 14px; |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | white-space: nowrap; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .type-tab:hover { |
| | | background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1)); |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .type-tab.active { |
| | | background: var(--el-bg-color, #fff); |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-weight: 600; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .tab-count { |
| | | min-width: 16px; |
| | | height: 16px; |
| | | padding: 0 5px; |
| | | background: var(--el-color-success, #67C23A); |
| | | color: #fff; |
| | | border-radius: 8px; |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .config-card { |
| | | margin-bottom: 16px; |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .type-icon { |
| | | width: 44px; |
| | | height: 44px; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .header-info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .type-name { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .header-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .flow-wrapper { |
| | | overflow-x: auto; |
| | | padding: 8px 4px; |
| | | } |
| | | |
| | | .flow-container { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 0; |
| | | min-width: min-content; |
| | | } |
| | | |
| | | .flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .node-card { |
| | | width: 200px; |
| | | background: var(--el-bg-color, #fff); |
| | | border: 2px solid var(--el-border-color, #e4e7ed); |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .node-card:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .node-card.empty { |
| | | border-style: dashed; |
| | | border-color: var(--el-border-color, #c0c4cc); |
| | | background: var(--el-fill-color-light, #fafbfc); |
| | | } |
| | | |
| | | .node-card.empty:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | } |
| | | |
| | | .node-badge { |
| | | position: absolute; |
| | | top: -10px; |
| | | left: 16px; |
| | | width: 24px; |
| | | height: 24px; |
| | | background: var(--el-color-primary, #409EFF); |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4); |
| | | } |
| | | |
| | | .node-avatar-section { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .node-avatar { |
| | | width: 56px; |
| | | height: 56px; |
| | | border-radius: 50%; |
| | | background: var(--el-fill-color, #f0f2f5); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | color: var(--el-text-color-placeholder, #c0c4cc); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .node-avatar.has-user { |
| | | color: #fff; |
| | | font-size: 22px; |
| | | font-weight: 600; |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .node-level { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .node-select-section { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .node-select-section :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .action-btn { |
| | | transition: all 0.2s; |
| | | } |
| | | |
| | | .action-btn:hover:not(:disabled) { |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .arrow-connector { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 50px; |
| | | position: relative; |
| | | } |
| | | |
| | | .arrow-line { |
| | | flex: 1; |
| | | height: 2px; |
| | | background: var(--el-border-color, #c0c4cc); |
| | | } |
| | | |
| | | .arrow-icon { |
| | | color: var(--el-text-color-placeholder, #c0c4cc); |
| | | font-size: 14px; |
| | | margin-left: -2px; |
| | | } |
| | | |
| | | /* æ°å¢èç¹æ ·å¼ */ |
| | | .add-node-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .add-node-card { |
| | | width: 140px; |
| | | height: 200px; |
| | | border: 2px dashed var(--el-border-color, #c0c4cc); |
| | | border-radius: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | background: var(--el-fill-color-light, #fafbfc); |
| | | flex-shrink: 0; |
| | | margin-left: 0; |
| | | } |
| | | |
| | | .add-node-card:hover { |
| | | border-color: var(--el-color-primary, #409EFF); |
| | | background: var(--el-color-primary-light-9, #f0f7ff); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15); |
| | | } |
| | | |
| | | .add-icon-wrapper { |
| | | width: 48px; |
| | | height: 48px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary, #409EFF); |
| | | color: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .add-node-card:hover .add-icon-wrapper { |
| | | transform: scale(1.1); |
| | | box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4); |
| | | } |
| | | |
| | | .add-text { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .add-node-card:hover .add-text { |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | /* ç©ºç¶æ */ |
| | | .empty-state { |
| | | padding: 50px 20px; |
| | | } |
| | | |
| | | .empty-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .empty-icon-wrapper { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 50%; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .empty-subtext { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | } |
| | | |
| | | .empty-add-btn { |
| | | margin-top: 8px; |
| | | padding: 12px 28px; |
| | | } |
| | | |
| | | /* åºé¨æç¤º */ |
| | | .bottom-tips { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding: 12px 20px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .bottom-tips .el-icon { |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-size: 16px; |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .type-tabs { |
| | | padding: 3px; |
| | | } |
| | | |
| | | .type-tab { |
| | | padding: 6px 10px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .flow-container { |
| | | flex-wrap: wrap; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .arrow-connector { |
| | | width: 100%; |
| | | height: 30px; |
| | | flex-direction: row; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .arrow-line { |
| | | width: 2px; |
| | | height: 30px; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | right: auto; |
| | | top: auto; |
| | | bottom: -5px; |
| | | transform: rotate(90deg); |
| | | } |
| | | |
| | | .add-node-item { |
| | | width: 100%; |
| | | justify-content: center; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .add-node-item .arrow-connector { |
| | | display: none; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 审æ¹äººéæ©ï¼å¨æèç¹ï¼ --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·äººï¼" prop="approveUser"> |
| | | <el-select |
| | | v-model="form.approveUser" |
| | | placeholder="éæ©äººå" |
| | | disabled |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·æ¥æï¼" prop="approveTime"> |
| | | <el-date-picker |
| | | v-model="form.approveTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- æ¥ä»·å®¡æ¹ï¼å±ç¤ºæ¥ä»·è¯¦æ
ï¼å¤ç¨é宿¥ä»·"æ¥ç详æ
å¯¹è¯æ¡"å
å®¹ç»æï¼ --> |
| | |
| | | updateApproveNode |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const userList = ref([]) |
| | | const quotationLoading = ref(false) |
| | | const currentQuotation = ref({}) |
| | | const purchaseLoading = ref(false) |
| | |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {} |
| | | currentPurchase.value = {} |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | // ç«å³æ¸
é¤è¡¨åéªè¯ç¶æï¼å ä¸ºåæ®µæ¯disabledçï¼ä¸éè¦éªè¯ï¼ |
| | | nextTick(() => { |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 审æ¹äººéæ©ï¼å¨æèç¹ï¼ --> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">æ°å¢èç¹</el-button> |
| | | </template> |
| | | <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | style="margin-right: 30px; text-align: center; margin-bottom: 10px;" |
| | | > |
| | | <div> |
| | | <span>审æ¹äºº</span> |
| | | â |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | style="width: 120px; margin-bottom: 8px;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·äººï¼" prop="approveUser"> |
| | | <el-select |
| | | v-model="form.approveUser" |
| | | placeholder="éæ©äººå" |
| | | filterable |
| | | default-first-option |
| | | :reserve-keyword="false" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·æ¥æï¼" prop="approveTime"> |
| | | <el-date-picker |
| | | v-model="form.approveTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="éä»¶ææï¼" prop="remark"> |
| | |
| | | import { |
| | | delLedgerFile, |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { getToken } from "@/utils/auth"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | import log from "@/views/monitor/job/log.vue"; |
| | | const userStore = useUserStore(); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | |
| | | }); |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveDeptId: "", |
| | | approveDeptName: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | tempFileIds: [], |
| | | approverList: [], // æ°å¢å段ï¼å卿æèç¹ç审æ¹äººid |
| | | startDate: "", // 请åå¼å§æ¶é´ |
| | | endDate: "", // 请åç»ææ¶é´ |
| | | price: null, // æ¥ééé¢ |
| | | location: "" // åºå·®å°ç¹ |
| | | }, |
| | | rules: { |
| | | approveTime: [{ required: false, message: "请è¾å
¥", trigger: "change" },], |
| | | approveId: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveUser: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveDeptName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | approveReason: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | checkResult: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | |
| | | } |
| | | }) |
| | | |
| | | // 审æ¹äººèç¹ç¸å
³ |
| | | const approverNodes = ref([ |
| | | { id: 1, userId: null } |
| | | ]) |
| | | let nextApproverId = 2 |
| | | const userList = ref([]) |
| | | function addApproverNode() { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }) |
| | | } |
| | | function removeApproverNode(index) { |
| | | approverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | // å¤çé¨é¨éæ©åå |
| | | const handleDeptChange = (deptId) => { |
| | | if (deptId) { |
| | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {} |
| | | approverNodes.value = [ |
| | | { id: 1, userId: null } |
| | | ] |
| | | form.value.approveUser = userStore.id; |
| | | form.value.approveTime = getCurrentDate(); |
| | | |
| | | form.value = {} |
| | | |
| | | // è·åå½åç¨æ·ä¿¡æ¯å¹¶è®¾ç½®é¨é¨ID |
| | | form.value.approveDeptId = userStore.currentDeptId |
| | | |
| | | |
| | | // å è½½é¨é¨é项ï¼å¹¶å¨å è½½å®æå设置é¨é¨åç§° |
| | | getProductOptions(); |
| | | if (operationType.value === 'edit') { |
| | | fileList.value = row.commonFileList |
| | | form.value.tempFileIds = fileList.value.map(file => file.id) |
| | | currentApproveStatus.value = row.approveStatus |
| | | currentApproveStatus.value = row.approveStatus |
| | | approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => { |
| | | form.value = {...res.data} |
| | | // 忾审æ¹äºº |
| | | if (res.data && res.data.approveUserIds) { |
| | | const userIds = res.data.approveUserIds.split(',') |
| | | approverNodes.value = userIds.map((userId, idx) => ({ |
| | | id: idx + 1, |
| | | userId: parseInt(userId.trim()) |
| | | })) |
| | | nextApproverId = userIds.length + 1 |
| | | } else { |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | } |
| | | form.value = {...res.data} |
| | | }) |
| | | } |
| | | } |
| | |
| | | } |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | // æ¶éææèç¹ç审æ¹äººid |
| | | form.value.approveUserIds = approverNodes.value.map(node => node.userId).join(',') |
| | | form.value.approveType = props.approveType |
| | | // 审æ¹äººå¿
å¡«æ ¡éª |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼") |
| | | return |
| | | } |
| | | // å½ approveType 为 2 æ¶ï¼æ ¡éªè¯·åæ¶é´ |
| | | if (props.approveType == 2) { |
| | | if (!form.value.startDate) { |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æ ç¾é¡µåæ¢ä¸åç审æ¹ç±»å --> |
| | | <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="approval-tabs"> |
| | | <el-tab-pane label="å
¬åºç®¡ç" name="1"></el-tab-pane> |
| | | <el-tab-pane label="请å管ç" name="2"></el-tab-pane> |
| | | <el-tab-pane label="åºå·®ç®¡ç" name="3"></el-tab-pane> |
| | | <el-tab-pane label="æ¥é管ç" name="4"></el-tab-pane> |
| | | <el-tab-pane label="éè´å®¡æ¹" name="5"></el-tab-pane> |
| | | <el-tab-pane label="æ¥ä»·å®¡æ¹" name="6"></el-tab-pane> |
| | | <el-tab-pane label="å货审æ¹" name="7"></el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">æµç¨ç¼å·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.approveId" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥æµç¨ç¼å·æç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span class="search_title ml10">审æ¹ç¶æï¼</span> |
| | | <el-select v-model="searchForm.approveStatus" clearable @change="handleQuery" style="width: 240px"> |
| | | <el-option label="å¾
å®¡æ ¸" :value="0" /> |
| | | <el-option label="å®¡æ ¸ä¸" :value="1" /> |
| | | <el-option label="å®¡æ ¸å®æ" :value="2" /> |
| | | <el-option label="å®¡æ ¸æªéè¿" :value="3" /> |
| | | <el-option label="已鿰æäº¤" :value="4" /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button |
| | | type="primary" |
| | | @click="openForm('add')" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | >æ°å¢</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | @click="handleDelete" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7" |
| | | >å é¤</el-button> |
| | | <!-- 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ --> |
| | | <div class="type-tabs"> |
| | | <div |
| | | v-for="type in approveTypes" |
| | | :key="type.value" |
| | | class="type-tab" |
| | | :class="{ active: activeTab === type.value }" |
| | | @click="activeTab = type.value; handleTabChange()" |
| | | > |
| | | <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }"> |
| | | <component :is="type.icon" /> |
| | | </el-icon> |
| | | <span class="tab-name">{{ type.label }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | |
| | | <!-- æç´¢åæä½åºå --> |
| | | <el-card class="search-card" shadow="never"> |
| | | <div class="search-content"> |
| | | <div class="search-filters"> |
| | | <div class="filter-item"> |
| | | <span class="filter-label">æµç¨ç¼å·</span> |
| | | <el-input |
| | | v-model="searchForm.approveId" |
| | | placeholder="请è¾å
¥æµç¨ç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="handleQuery" |
| | | class="search-input" |
| | | /> |
| | | </div> |
| | | <div class="filter-item"> |
| | | <span class="filter-label">审æ¹ç¶æ</span> |
| | | <el-select |
| | | v-model="searchForm.approveStatus" |
| | | clearable |
| | | @change="handleQuery" |
| | | placeholder="è¯·éæ©ç¶æ" |
| | | class="search-select" |
| | | > |
| | | <el-option label="å¾
å®¡æ ¸" :value="0"> |
| | | <el-tag size="small" type="warning">å¾
å®¡æ ¸</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸ä¸" :value="1"> |
| | | <el-tag size="small" type="primary">å®¡æ ¸ä¸</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸å®æ" :value="2"> |
| | | <el-tag size="small" type="success">å®¡æ ¸å®æ</el-tag> |
| | | </el-option> |
| | | <el-option label="å®¡æ ¸æªéè¿" :value="3"> |
| | | <el-tag size="small" type="danger">å®¡æ ¸æªéè¿</el-tag> |
| | | </el-option> |
| | | <el-option label="已鿰æäº¤" :value="4"> |
| | | <el-tag size="small" type="info">已鿰æäº¤</el-tag> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | <el-button type="primary" @click="handleQuery" class="search-btn"> |
| | | <el-icon><Search /></el-icon> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="resetQuery" class="reset-btn"> |
| | | <el-icon><RefreshRight /></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | </div> |
| | | <div class="search-actions"> |
| | | <el-button |
| | | type="primary" |
| | | @click="openForm('add')" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 10" |
| | | class="action-btn primary" |
| | | > |
| | | <el-icon><Plus /></el-icon> |
| | | æ°å¢ |
| | | </el-button> |
| | | <el-button @click="handleOut" class="action-btn"> |
| | | <el-icon><Download /></el-icon> |
| | | å¯¼åº |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | @click="handleDelete" |
| | | v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7 && currentApproveType !== 10" |
| | | class="action-btn danger" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- æ°æ®è¡¨æ ¼ --> |
| | | <el-card class="table-card" shadow="never" v-loading="tableLoading"> |
| | | <template #header> |
| | | <div class="table-header"> |
| | | <div class="table-title"> |
| | | <div class="type-tag" :style="{ backgroundColor: currentTypeInfo.color }"> |
| | | <el-icon color="#fff" :size="16"><component :is="currentTypeInfo.icon" /></el-icon> |
| | | </div> |
| | | <span>{{ currentTypeInfo.label }}å表</span> |
| | | <el-tag type="info" size="small" effect="plain" class="count-tag"> |
| | | å
± {{ page.total }} æ¡ |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumnCopy" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | rowKey="id" |
| | | :column="tableColumnCopy" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | class="custom-table" |
| | | ></PIMTable> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- å¼¹çªç»ä»¶ --> |
| | | <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia> |
| | | <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia> |
| | | <FileList ref="fileListRef" /> |
| | |
| | | |
| | | <script setup> |
| | | import FileList from "./fileList.vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import { useRoute } from 'vue-router'; |
| | |
| | | // å½åéä¸çæ ç¾é¡µï¼é»è®¤ä¸ºå
¬åºç®¡ç |
| | | const activeTab = ref('1'); |
| | | |
| | | // åç±»åæ°éç»è®¡ |
| | | const typeCounts = ref({}); |
| | | |
| | | // 审æ¹ç±»åé
ç½® |
| | | const approveTypes = [ |
| | | { value: '1', label: 'å
¬åºç®¡ç', icon: 'Suitcase', color: '#409EFF' }, |
| | | { value: '2', label: '请å管ç', icon: 'Calendar', color: '#67C23A' }, |
| | | { value: '3', label: 'åºå·®ç®¡ç', icon: 'Location', color: '#E6A23C' }, |
| | | { value: '4', label: 'æ¥é管ç', icon: 'Money', color: '#F56C6C' }, |
| | | { value: '5', label: 'éè´å®¡æ¹', icon: 'ShoppingCart', color: '#909399' }, |
| | | { value: '6', label: 'æ¥ä»·å®¡æ¹', icon: 'DocumentChecked', color: '#9B59B6' }, |
| | | { value: '7', label: 'å货审æ¹', icon: 'Van', color: '#1ABC9C' }, |
| | | { value: '10', label: 'éå®å®¡æ¹', icon: 'Sell', color: '#FF6B6B' }, |
| | | ]; |
| | | |
| | | // å½å审æ¹ç±»åä¿¡æ¯ |
| | | const currentTypeInfo = computed(() => { |
| | | return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0]; |
| | | }); |
| | | |
| | | // è·åç±»åæ°é |
| | | const getTypeCount = (value) => { |
| | | return typeCounts.value[value] || 0; |
| | | }; |
| | | |
| | | // å½å审æ¹ç±»åï¼æ ¹æ®éä¸çæ ç¾é¡µè®¡ç® |
| | | const currentApproveType = computed(() => { |
| | | return Number(activeTab.value); |
| | | }); |
| | | |
| | | // æ ç¾é¡µåæ¢å¤ç |
| | | const handleTabChange = (tabName) => { |
| | | const handleTabChange = () => { |
| | | // 忢æ ç¾é¡µæ¶éç½®æç´¢æ¡ä»¶åå页ï¼å¹¶éæ°å è½½æ°æ® |
| | | searchForm.value.approveId = ''; |
| | | searchForm.value.approveStatus = ''; |
| | |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | approveId: "", |
| | | approveStatus: "", |
| | | approveId: "", |
| | | approveStatus: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | // éç½®æç´¢ |
| | | const resetQuery = () => { |
| | | searchForm.value.approveId = ''; |
| | | searchForm.value.approveStatus = ''; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // å¨æè¡¨æ ¼åé
ç½®ï¼æ ¹æ®å®¡æ¹ç±»åçæå |
| | | const tableColumnCopy = computed(() => { |
| | |
| | | const isReimburseType = currentApproveType.value === 4; // æ¥é管ç |
| | | const isQuotationType = currentApproveType.value === 6; // æ¥ä»·å®¡æ¹ |
| | | const isPurchaseType = currentApproveType.value === 5; // éè´å®¡æ¹ |
| | | const isSalesType = currentApproveType.value === 10; // éå®å®¡æ¹ |
| | | |
| | | // åºç¡åé
ç½® |
| | | const baseColumns = [ |
| | |
| | | currentApproveType.value === 5 || |
| | | currentApproveType.value === 6 || |
| | | currentApproveType.value === 7 || |
| | | currentApproveType.value === 10 || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 1 || |
| | | row.approveStatus == 4 |
| | |
| | | }, |
| | | ]; |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç±»å 6ï¼ä¸å±ç¤ºâéä»¶âæä½ |
| | | // æ¥ä»·å®¡æ¹ï¼ç±»å 6ï¼ä¸å±ç¤º"éä»¶"æä½ |
| | | if (!isQuotationType) { |
| | | actionOperations.push({ |
| | | name: "éä»¶", |
| | |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | // æ´æ°å½åç±»åæ°é |
| | | typeCounts.value[activeTab.value] = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | |
| | | 5: "/approveProcess/exportFive", |
| | | 6: "/approveProcess/exportSix", |
| | | 7: "/approveProcess/exportSeven", |
| | | 10: "/approveProcess/exportTen", |
| | | } |
| | | const url = urlMap[type] || urlMap[0] |
| | | const nameMap = { |
| | |
| | | 5: "éè´ç³è¯·å®¡æ¹è¡¨", |
| | | 6: "æ¥ä»·å®¡æ¹è¡¨", |
| | | 7: "å货审æ¹è¡¨", |
| | | 10: "éå®å®¡æ¹è¡¨", |
| | | } |
| | | const fileName = nameMap[type] || nameMap[0] |
| | | proxy.download(url, {}, `${fileName}.xlsx`) |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approval-tabs { |
| | | margin-bottom: 10px; |
| | | .page-header { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .header-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .title-icon { |
| | | font-size: 28px; |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .header-text { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .main-title { |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .sub-title { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary, #909399); |
| | | } |
| | | |
| | | /* 审æ¹ç±»å忢 - ç´§åæ ç¾å¼ */ |
| | | .type-tabs { |
| | | display: flex; |
| | | gap: 4px; |
| | | margin-bottom: 16px; |
| | | padding: 4px; |
| | | background: var(--el-fill-color-light, #f5f7fa); |
| | | border-radius: 8px; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | .type-tab { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 14px; |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | white-space: nowrap; |
| | | font-size: 13px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | } |
| | | |
| | | .type-tab:hover { |
| | | background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1)); |
| | | color: var(--el-color-primary, #409EFF); |
| | | } |
| | | |
| | | .type-tab.active { |
| | | background: var(--el-bg-color, #fff); |
| | | color: var(--el-color-primary, #409EFF); |
| | | font-weight: 600; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .tab-count { |
| | | min-width: 16px; |
| | | height: 16px; |
| | | padding: 0 5px; |
| | | background: var(--el-color-success, #67C23A); |
| | | color: #fff; |
| | | border-radius: 8px; |
| | | font-size: 11px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* æç´¢å¡ç */ |
| | | .search-card { |
| | | margin-bottom: 16px; |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .search-content { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .search-filters { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular, #606266); |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .search-input, |
| | | .search-select { |
| | | width: 200px; |
| | | } |
| | | |
| | | .search-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .reset-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .search-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .action-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .action-btn.primary { |
| | | background: var(--el-color-primary, #409EFF); |
| | | border: none; |
| | | } |
| | | |
| | | .action-btn.danger { |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .action-btn.danger:hover { |
| | | background: #f56c6c; |
| | | color: #fff; |
| | | } |
| | | |
| | | /* è¡¨æ ¼å¡ç */ |
| | | .table-card { |
| | | border-radius: 12px; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 16px 20px; |
| | | border-bottom: 1px solid var(--el-border-color-light, #ebeef5); |
| | | } |
| | | |
| | | .table-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .table-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary, #303133); |
| | | } |
| | | |
| | | .type-tag { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .count-tag { |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .custom-table { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | /* ååºå¼ */ |
| | | @media (max-width: 1200px) { |
| | | .search-content { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .search-filters { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .search-actions { |
| | | justify-content: flex-end; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .type-tabs { |
| | | padding: 3px; |
| | | } |
| | | |
| | | .type-tab { |
| | | padding: 6px 10px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .tab-name { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .search-filters { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .filter-item { |
| | | width: 100%; |
| | | } |
| | | |
| | | .search-input, |
| | | .search-select { |
| | | width: 100%; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;"> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" size="small" @click="addApproverNode" icon="Plus">æ°å¢èç¹</el-button> |
| | | </div> |
| | | </template> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-header"> |
| | | <span class="approver-node-label">审æ¹èç¹ {{ index + 1 }}</span> |
| | | <el-button |
| | | v-if="approverNodes.length > 1" |
| | | type="danger" |
| | | size="small" |
| | | text |
| | | @click="removeApproverNode(index)" |
| | | icon="Delete" |
| | | >å é¤</el-button> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="è¯·éæ©å®¡æ¹äºº" |
| | | filterable |
| | | style="width: 100%;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-form-item label="产åä¿¡æ¯ï¼" |
| | | prop="entryDate"> |
| | |
| | | |
| | | const userStore = useUserStore(); |
| | | |
| | | // 审æ¹äººèç¹ï¼ä»¿éå®å°è´¦å货审æ¹äººï¼ |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | | let nextApproverId = 2; |
| | | const addApproverNode = () => { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }); |
| | | }; |
| | | const removeApproverNode = (index) => { |
| | | approverNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | // 订å审æ¹ç¶ææ¾ç¤ºææ¬ |
| | | const approvalStatusText = { |
| | | 1: "å¾
å®¡æ ¸", |
| | |
| | | } |
| | | |
| | | try { |
| | | // è·å审æ¹äººIDå符串 |
| | | const approveUserIds = approverNodes.value |
| | | .filter(node => node.userId) |
| | | .map(node => node.userId) |
| | | .join(","); |
| | | |
| | | let params = { |
| | | productData: proxy.HaveJson(productData.value), |
| | | supplierId: form.value.supplierId, |
| | | paymentMethod: form.value.paymentMethod, |
| | | recorderId: form.value.recorderId, |
| | | projectName: form.value.projectName, |
| | | approveUserIds: approveUserIds, |
| | | templateName: templateName.value.trim(), |
| | | }; |
| | | console.log("template params ===>", params, "currentTemplateId:", currentTemplateId.value); |
| | |
| | | templateName.value = ""; |
| | | filterInputValue.value = ""; |
| | | isTemplateNameDuplicate.value = false; |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | try { |
| | | // å¹¶è¡å è½½åºç¡æ°æ® |
| | | const [userRes, salesRes, supplierRes] = await Promise.all([ |
| | |
| | | form.value = { ...purchaseRes }; |
| | | productData.value = purchaseRes.productData || []; |
| | | fileList.value = purchaseRes.salesLedgerFiles || []; |
| | | // 妿ç¼è¾æ¶æå®¡æ¹äººï¼è§£æå®¡æ¹äººIDå符串并设置å°èç¹ä¸ |
| | | if (purchaseRes.approveUserIds) { |
| | | const approverIds = purchaseRes.approveUserIds.split(","); |
| | | approverNodes.value = approverIds.map((id, index) => ({ |
| | | id: index + 1, |
| | | userId: Number(id) |
| | | })); |
| | | nextApproverId = approverIds.length + 1; |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½éè´å°è´¦æ°æ®å¤±è´¥:", error); |
| | | proxy.$modal.msgError("å è½½æ°æ®å¤±è´¥"); |
| | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // 审æ¹äººå¿
å¡«æ ¡éªï¼ææèç¹é½è¦éäººï¼ |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId); |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼"); |
| | | return; |
| | | } |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | |
| | | if (productData.value.length > 0) { |
| | | // æ°å¢æ¶ï¼éè¦ä»æ¯ä¸ªäº§å对象ä¸å é¤ id åæ®µ |
| | | let processedProductData = productData.value; |
| | |
| | | } |
| | | form.value.tempFileIds = tempFileIds; |
| | | form.value.type = 2; |
| | | form.value.approveUserIds = approveUserIds; |
| | | |
| | | // 妿salesLedgerId为空ï¼åä¸ä¼ ésalesContractNo |
| | | if (!form.value.salesLedgerId) { |
| | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | // æå¼äº§åå¼¹æ¡ |
| | |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="ç¼è¾å·¥èºè·¯çº¿" |
| | | width="400" |
| | | width="800" |
| | | @close="closeModal" |
| | | > |
| | | <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> |
| | |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="BOM" |
| | | prop="bomId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©BOM', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!formState.productModelId || bomOptions.length === 0" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="description"> |
| | | <el-input v-model="formState.description" type="textarea" /> |
| | | </el-form-item> |
| | | |
| | | <!-- å·¥åºé
ç½® --> |
| | | <el-form-item label="å·¥åºé
ç½®" required> |
| | | <el-table :data="formState.processRouteItemList" border size="small" style="width: 100%"> |
| | | <el-table-column label="é¨ä»¶" min-width="200"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-model="row.processId" |
| | | placeholder="è¯·éæ©é¨ä»¶" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="process in processOptions" |
| | | :key="process.id" |
| | | :label="formatProcessOptionLabel(process)" |
| | | :value="process.id" |
| | | /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è´¨æ£" width="80" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-switch v-model="row.isQuality" :active-value="true" inactive-value="false"/> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="夿³¨" min-width="150"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model="row.remark" placeholder="请è¾å
¥å¤æ³¨" size="small"/> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="60" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button type="danger" link size="small" @click="removeProcessItem($index)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div style="margin-top: 8px;"> |
| | | <el-button type="primary" link size="small" @click="addProcessItem"> |
| | | <el-icon><Plus /></el-icon> æ·»å å·¥åº |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance, onMounted, nextTick, watch} from "vue"; |
| | | import {update} from "@/api/productionManagement/processRoute.js"; |
| | | import {getByModel} from "@/api/productionManagement/productBom.js"; |
| | | import {processList} from "@/api/productionManagement/productionProcess.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: '', |
| | | processRouteItemList: [], |
| | | }); |
| | | |
| | | const isShow = computed({ |
| | |
| | | }); |
| | | |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | const processOptions = ref([]); |
| | | |
| | | let { proxy } = getCurrentInstance() |
| | | |
| | | // è·åå·¥åºå表 |
| | | const getProcessOptions = async () => { |
| | | try { |
| | | const res = await processList(); |
| | | processOptions.value = res.data || []; |
| | | } catch (error) { |
| | | console.error("è·åå·¥åºå表失败", error); |
| | | processOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // æ ¼å¼åå·¥åºé项æ ç¾ |
| | | const formatProcessOptionLabel = (process) => { |
| | | if (!process) return ''; |
| | | const typeMap = { |
| | | 1: 'å å·¥', |
| | | 2: '宿¿å·è¯å¶ä½', |
| | | 3: '管路ç»å¯¹', |
| | | 4: 'ç½ä½è¿æ¥åè°è¯', |
| | | 5: 'æµè¯æå', |
| | | 6: 'å
¶ä»', |
| | | }; |
| | | const typeText = typeMap[process.type] || ''; |
| | | return `${process.name} ${process.no ? '(' + process.no + ')' : ''} ${typeText ? '[' + typeText + ']' : ''}`; |
| | | }; |
| | | |
| | | // æ·»å å·¥åº |
| | | const addProcessItem = () => { |
| | | formState.value.processRouteItemList.push({ |
| | | processId: undefined, |
| | | isQuality: false, |
| | | remark: '', |
| | | }); |
| | | }; |
| | | |
| | | // ç§»é¤å·¥åº |
| | | const removeProcessItem = (index) => { |
| | | formState.value.processRouteItemList.splice(index, 1); |
| | | }; |
| | | |
| | | const closeModal = () => { |
| | | isShow.value = false; |
| | |
| | | productName: props.record.productName || "", |
| | | // 注æï¼recordä¸çåæ®µæ¯modelï¼éè¦æ å°å°productModelName |
| | | productModelName: props.record.model || props.record.productModelName || "", |
| | | bomId: props.record.bomId, |
| | | description: props.record.description || '', |
| | | processRouteItemList: props.record.processRouteItemList || [], |
| | | }; |
| | | // 妿æäº§ååå·IDï¼å è½½BOMå表 |
| | | if (props.record.productModelId) { |
| | | loadBomList(props.record.productModelId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å è½½BOMå表 |
| | | const loadBomList = async (productModelId) => { |
| | | if (!productModelId) { |
| | | bomOptions.value = []; |
| | | return; |
| | | } |
| | | try { |
| | | const res = await getByModel(productModelId); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === 'object') { |
| | | bomList = [res]; |
| | | } |
| | | bomOptions.value = bomList; |
| | | } catch (error) { |
| | | console.error("å è½½BOMå表失败ï¼", error); |
| | | bomOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // 产åéæ©å¤ç |
| | | const handleProductSelect = async (products) => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === 'object') { |
| | | bomList = [res]; |
| | | } |
| | | |
| | | if (bomList.length > 0) { |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | // 妿å½åéæ©çBOMä¸å¨æ°å表ä¸ï¼åéç½®BOMéæ© |
| | | const currentBomExists = bomList.some(bom => bom.id === formState.value.bomId); |
| | | if (!currentBomExists) { |
| | | formState.value.bomId = undefined; |
| | | } |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // éªè¯æ¯å¦éæ©äºäº§ååBOM |
| | | // éªè¯æ¯å¦éæ©äºäº§å |
| | | if (!formState.value.productModelId) { |
| | | proxy.$modal.msgError("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | if (!formState.value.bomId) { |
| | | proxy.$modal.msgError("è¯·éæ©BOM"); |
| | | // éªè¯æ¯å¦é
ç½®äºå·¥åº |
| | | if (!formState.value.processRouteItemList || formState.value.processRouteItemList.length === 0) { |
| | | proxy.$modal.msgError("请è³å°é
ç½®ä¸ä¸ªå·¥åº"); |
| | | return; |
| | | } |
| | | // éªè¯ææå·¥åºæ¯å¦éæ©äºé¨ä»¶ |
| | | const invalidItem = formState.value.processRouteItemList.find(item => !item.processId); |
| | | if (invalidItem) { |
| | | proxy.$modal.msgError("è¯·éæ©ææå·¥åºçé¨ä»¶"); |
| | | return; |
| | | } |
| | | update(formState.value).then(res => { |
| | |
| | | }, { immediate: true }); |
| | | |
| | | onMounted(() => { |
| | | getProcessOptions(); |
| | | if (props.visible && props.record) { |
| | | setFormData(); |
| | | } |
| | |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="BOM" |
| | | prop="bomId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©BOM', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.bomId" |
| | | placeholder="è¯·éæ©BOM" |
| | | clearable |
| | | :disabled="!formState.productModelId || bomOptions.length === 0" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in bomOptions" |
| | | :key="item.id" |
| | | :label="item.bomNo || `BOM-${item.id}`" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="description"> |
| | | <el-input v-model="formState.description" type="textarea" /> |
| | | </el-form-item> |
| | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance} from "vue"; |
| | | import {add} from "@/api/productionManagement/processRoute.js"; |
| | | import {getByModel} from "@/api/productionManagement/productBom.js"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: '', |
| | | }); |
| | | |
| | |
| | | }); |
| | | |
| | | const showProductSelectDialog = ref(false); |
| | | const bomOptions = ref([]); |
| | | |
| | | let { proxy } = getCurrentInstance() |
| | | |
| | |
| | | productModelId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | bomId: undefined, |
| | | description: '', |
| | | }; |
| | | bomOptions.value = []; |
| | | isShow.value = false; |
| | | }; |
| | | |
| | |
| | | const handleProductSelect = async (products) => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | // å
æ¥è¯¢BOMå表ï¼å¿
éï¼ |
| | | try { |
| | | const res = await getByModel(product.id); |
| | | // å¤çè¿åçBOMæ°æ®ï¼å¯è½æ¯æ°ç»ã对象æå
å«dataåæ®µ |
| | | let bomList = []; |
| | | if (Array.isArray(res)) { |
| | | bomList = res; |
| | | } else if (res && res.data) { |
| | | bomList = Array.isArray(res.data) ? res.data : [res.data]; |
| | | } else if (res && typeof res === 'object') { |
| | | bomList = [res]; |
| | | } |
| | | |
| | | if (bomList.length > 0) { |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | formState.value.bomId = undefined; // éç½®BOMéæ© |
| | | bomOptions.value = bomList; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } else { |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | } catch (error) { |
| | | // 妿æ¥å£è¿å404æå
¶ä»é误ï¼è¯´ææ²¡æBOM |
| | | proxy.$modal.msgError("è¯¥äº§åæ²¡æBOMï¼è¯·å
å建BOM"); |
| | | } |
| | | formState.value.productModelId = product.id; |
| | | formState.value.productName = product.productName; |
| | | formState.value.productModelName = product.model; |
| | | showProductSelectDialog.value = false; |
| | | // 触å表åéªè¯æ´æ° |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } |
| | | }; |
| | | |
| | | const handleSubmit = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // éªè¯æ¯å¦éæ©äºäº§ååBOM |
| | | // éªè¯æ¯å¦éæ©äºäº§å |
| | | if (!formState.value.productModelId) { |
| | | proxy.$modal.msgError("è¯·éæ©äº§å"); |
| | | return; |
| | | } |
| | | if (!formState.value.bomId) { |
| | | proxy.$modal.msgError("è¯·éæ©BOM"); |
| | | return; |
| | | } |
| | | add(formState.value).then(res => { |
| | |
| | | }) |
| | | }; |
| | | |
| | | |
| | | defineExpose({ |
| | | closeModal, |
| | | handleSubmit, |
| | | isShow, |
| | | }); |
| | | </script> |
| | | |
| | | |
| | |
| | | |
| | | <!-- è¡¨æ ¼è§å¾ --> |
| | | <div v-if="viewMode === 'table'" class="section-header"> |
| | | <div class="section-title">产åé¨ä»¶å表</div> |
| | | <div class="section-title">å·¥åºå表</div> |
| | | <div class="section-actions"> |
| | | <el-button |
| | | icon="Grid" |
| | |
| | | class="lims-table" |
| | | > |
| | | <el-table-column align="center" label="åºå·" width="60" type="index" /> |
| | | <el-table-column label="产ååç§°" prop="name" min-width="140" show-overflow-tooltip> |
| | | <el-table-column label="é¨ä»¶åç§°" prop="name" min-width="140" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'name') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产åè§æ ¼" prop="productModel" min-width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessField(scope.row, 'productModel') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="é¨ä»¶ç¼å·" prop="no" width="120" show-overflow-tooltip> |
| | |
| | | <el-table-column label="é¨ä»¶ç±»å" prop="typeText" width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ getProcessTypeText(getProcessRaw(scope.row)?.type) || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产ååç§°" prop="productName" min-width="140" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ scope.row.productName || getProcessField(scope.row, 'productName') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产åè§æ ¼" prop="productModel" min-width="120" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ scope.row.model || getProcessField(scope.row, 'productModel') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计åå·¥æ¶(å°æ¶)" prop="salaryQuota" width="130" align="center"> |
| | |
| | | <!-- å¡çè§å¾ --> |
| | | <template v-else> |
| | | <div class="section-header"> |
| | | <div class="section-title">å·¥èºè·¯çº¿é¡¹ç®å表</div> |
| | | <div class="section-title">å·¥åºå表</div> |
| | | <div class="section-actions"> |
| | | <el-button |
| | | icon="Menu" |
| | |
| | | <!-- ä¸å·¥åºä¸»è¡¨ä¸è´çç®è¦ä¿¡æ¯ --> |
| | | <div class="card-content"> |
| | | <div class="product-info"> |
| | | <div class="product-name">{{ getProcessField(item, 'productModel') }}</div> |
| | | <div class="product-name">{{ item.productName || getProcessField(item, 'productName') }}</div> |
| | | <div class="product-model">{{ item.model || getProcessField(item, 'productModel') }}</div> |
| | | <div v-if="getProcessRaw(item)?.no" class="product-model">ç¼å· {{ getProcessRaw(item)?.no }}</div> |
| | | <div v-if="getProcessTypeText(getProcessRaw(item)?.type)" class="product-model"> |
| | | {{ getProcessTypeText(getProcessRaw(item)?.type) }} |
| | |
| | | <!-- æ°å¢/ç¼è¾å¼¹çªï¼å¸å±ãåæ®µä¸å·¥åº/é¨ä»¶é¡µ NewãEdit ä¸è´ï¼æäº¤åæ°ä»ä¸ºåæ¥å£åæ®µï¼ --> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢å·¥èºè·¯çº¿é¡¹ç®' : 'ç¼è¾å·¥èºè·¯çº¿é¡¹ç®'" |
| | | :title="operationType === 'add' ? 'æ°å¢å·¥åº' : 'ç¼è¾å·¥åº'" |
| | | width="760" |
| | | @close="closeDialog" |
| | | > |
| | |
| | | const submitLoading = ref(false); |
| | | const cardsContainer = ref(null); |
| | | const tableRef = ref(null); |
| | | const viewMode = ref('table'); // table | card |
| | | const viewMode = ref('card'); // table | card |
| | | const routeInfo = ref({ |
| | | processRouteCode: '', |
| | | productName: '', |
| | |
| | | <el-input v-model="formState.unit" disabled /> |
| | | </el-form-item> |
| | | |
| | | <!-- <el-form-item label="å·¥èºè·¯çº¿"> |
| | | <el-form-item label="å·¥èºè·¯çº¿"> |
| | | <el-select v-model="formState.routeId" |
| | | placeholder="è¯·éæ©å·¥èºè·¯çº¿" |
| | | style="width: 100%;" |
| | |
| | | :label="`${item.processRouteCode || ''}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> --> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="éæ±æ°é" |
| | |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产ååç§°ï¼" |
| | | prop="productId" |
| | | label="é¨ä»¶åç§°" |
| | | prop="name" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§ååç§°', |
| | | }, |
| | | ]"> |
| | | <el-tree-select |
| | | v-model="formState.productId" |
| | | placeholder="è¯·éæ©äº§ååç§°" |
| | | clearable |
| | | filterable |
| | | check-strictly |
| | | :data="productCategoryOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | @change="handleProductChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产åè§æ ¼ï¼" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§åè§æ ¼', |
| | | }, |
| | | ]"> |
| | | <el-select v-model="formState.productModelId" |
| | | placeholder="è¯·éæ©äº§åè§æ ¼" |
| | | clearable |
| | | filterable |
| | | :disabled="!formState.productId" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | message: '请è¾å
¥é¨ä»¶åç§°', |
| | | } |
| | | ]" |
| | | > |
| | | <el-input v-model="formState.name" placeholder="请è¾å
¥é¨ä»¶åç§°" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨ä»¶ç¼å·" prop="no"> |
| | | <el-input v-model="formState.no" /> |
| | | <el-input v-model="formState.no" placeholder="请è¾å
¥é¨ä»¶ç¼å·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è®¡åæ§è¡äººå" prop="executorId"> |
| | | <el-select |
| | | v-model="formState.executorId" |
| | | placeholder="è¯·éæ©è®¡åæ§è¡äººå" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | @change="handleExecutorChange" |
| | | > |
| | | <el-option |
| | | v-for="item in plannerOptions" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦è´¨æ£" prop="isQuality"> |
| | | <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> |
| | | </el-form-item> |
| | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance, watch, onMounted } from "vue"; |
| | | import {update} from "@/api/productionManagement/productionProcess.js"; |
| | | import { modelListPage, productTreeList } from "@/api/basicData/product"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | const formState = ref({ |
| | | id: props.record.id, |
| | | name: props.record.name, |
| | | productId: props.record.productId, |
| | | productModelId: props.record.productModelId, |
| | | type: props.record.type, |
| | | no: props.record.no, |
| | | remark: props.record.remark, |
| | | salaryQuota: props.record.salaryQuota, |
| | | plannerId: props.record.plannerId, |
| | | plannerName: props.record.plannerName, |
| | | executorId: props.record.executorId, |
| | | executorName: props.record.executorName, |
| | | isQuality: props.record.isQuality, |
| | | }); |
| | | const productCategoryOptions = ref([]); |
| | | const modelOptions = ref([]); |
| | | const plannerOptions = ref([]); |
| | | |
| | | const isShow = computed({ |
| | |
| | | formState.value = { |
| | | id: newRecord.id, |
| | | name: newRecord.name || '', |
| | | productId: newRecord.productId, |
| | | productModelId: getRecordProductModelId(newRecord), |
| | | no: newRecord.no || '', |
| | | type: newRecord.type, |
| | | no: newRecord.no || '', |
| | | remark: newRecord.remark || '', |
| | | salaryQuota: newRecord.salaryQuota || '', |
| | | plannerId: newRecord.plannerId, |
| | | plannerName: newRecord.plannerName || '', |
| | | executorId: newRecord.executorId, |
| | | executorName: newRecord.executorName || '', |
| | | isQuality: props.record.isQuality, |
| | | }; |
| | | } |
| | |
| | | formState.value = { |
| | | id: props.record.id, |
| | | name: props.record.name || '', |
| | | productId: props.record.productId, |
| | | productModelId: getRecordProductModelId(props.record), |
| | | no: props.record.no || '', |
| | | type: props.record.type, |
| | | no: props.record.no || '', |
| | | remark: props.record.remark || '', |
| | | salaryQuota: props.record.salaryQuota || '', |
| | | plannerId: props.record.plannerId, |
| | | plannerName: props.record.plannerName || '', |
| | | executorId: props.record.executorId, |
| | | executorName: props.record.executorName || '', |
| | | isQuality: props.record.isQuality, |
| | | }; |
| | | getModelOptions(formState.value.productId); |
| | | } |
| | | }); |
| | | |
| | |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | const convertProductTree = list => { |
| | | return (list || []).map(item => { |
| | | const children = convertProductTree(item.children || item.childList || []); |
| | | return { |
| | | ...item, |
| | | value: item.id, |
| | | label: item.name || item.label, |
| | | children, |
| | | disabled: children.length > 0, |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | const findNodeById = (nodes, targetId) => { |
| | | for (const node of nodes || []) { |
| | | if (String(node.value) === String(targetId)) { |
| | | return node; |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeById(node.children, targetId); |
| | | if (found) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const findNodeIdByLabel = (nodes, targetLabel) => { |
| | | for (const node of nodes || []) { |
| | | if (node.label === targetLabel) { |
| | | return node.value; |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, targetLabel); |
| | | if (found !== null && found !== undefined) return found; |
| | | } |
| | | } |
| | | return undefined; |
| | | }; |
| | | |
| | | function findModelIdByName(models, modelName) { |
| | | if (!modelName) { |
| | | return undefined; |
| | | } |
| | | const matched = (models || []).find(item => String(item.model) === String(modelName)); |
| | | return matched?.id; |
| | | } |
| | | |
| | | function getRecordProductModelId(record) { |
| | | if (!record) { |
| | | return undefined; |
| | | } |
| | | return record.productModelId ?? record.modelId ?? record.specificationModelId ?? undefined; |
| | | } |
| | | |
| | | function syncProductModelIdFromModelName() { |
| | | if (formState.value.productModelId || modelOptions.value.length === 0) { |
| | | return; |
| | | } |
| | | const modelName = props.record?.productModel || props.record?.model || props.record?.specificationModel || ""; |
| | | const matchedId = findModelIdByName(modelOptions.value, modelName); |
| | | if (matchedId !== undefined) { |
| | | formState.value.productModelId = matchedId; |
| | | } |
| | | } |
| | | |
| | | const getProductCategoryOptions = async () => { |
| | | try { |
| | | const res = await productTreeList(); |
| | | const list = Array.isArray(res) ? res : res?.data || []; |
| | | productCategoryOptions.value = convertProductTree(list); |
| | | if (!formState.value.productId && formState.value.name) { |
| | | formState.value.productId = findNodeIdByLabel(productCategoryOptions.value, formState.value.name); |
| | | } |
| | | await getModelOptions(formState.value.productId); |
| | | syncProductModelIdFromModelName(); |
| | | } catch (e) { |
| | | productCategoryOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getModelOptions = async productId => { |
| | | if (!productId) { |
| | | modelOptions.value = []; |
| | | return; |
| | | } |
| | | try { |
| | | const res = await modelListPage({ |
| | | id: productId, |
| | | current: 1, |
| | | size: 999, |
| | | }); |
| | | const records = res?.records || res?.data?.records || []; |
| | | modelOptions.value = records; |
| | | syncProductModelIdFromModelName(); |
| | | } catch (e) { |
| | | modelOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getPlannerOptions = async () => { |
| | |
| | | formState.value.plannerName = selectedUser?.nickName || ''; |
| | | }; |
| | | |
| | | const handleProductChange = async value => { |
| | | const selectedNode = findNodeById(productCategoryOptions.value, value); |
| | | formState.value.name = selectedNode?.label || ''; |
| | | formState.value.productModelId = undefined; |
| | | await getModelOptions(value); |
| | | const handleExecutorChange = value => { |
| | | const selectedUser = plannerOptions.value.find(item => String(item.userId) === String(value)); |
| | | formState.value.executorName = selectedUser?.nickName || ''; |
| | | }; |
| | | |
| | | const closeModal = () => { |
| | |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getProductCategoryOptions(); |
| | | getPlannerOptions(); |
| | | }); |
| | | |
| | |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产ååç§°ï¼" |
| | | prop="productId" |
| | | label="é¨ä»¶åç§°" |
| | | prop="name" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§ååç§°', |
| | | }, |
| | | ]"> |
| | | <el-tree-select |
| | | v-model="formState.productId" |
| | | placeholder="è¯·éæ©äº§ååç§°" |
| | | clearable |
| | | filterable |
| | | check-strictly |
| | | :data="productCategoryOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | @change="handleProductChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="产åè§æ ¼ï¼" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©äº§åè§æ ¼', |
| | | }, |
| | | ]"> |
| | | <el-select v-model="formState.productModelId" |
| | | placeholder="è¯·éæ©äº§åè§æ ¼" |
| | | clearable |
| | | filterable |
| | | :disabled="!formState.productId" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in modelOptions" |
| | | :key="item.id" |
| | | :label="item.model" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | message: '请è¾å
¥é¨ä»¶åç§°', |
| | | } |
| | | ]" |
| | | > |
| | | <el-input v-model="formState.name" placeholder="请è¾å
¥é¨ä»¶åç§°" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨ä»¶ç¼å·" prop="no"> |
| | | <el-input v-model="formState.no" /> |
| | | <el-input v-model="formState.no" placeholder="请è¾å
¥é¨ä»¶ç¼å·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è®¡åæ§è¡äººå" prop="executorId"> |
| | | <el-select |
| | | v-model="formState.executorId" |
| | | placeholder="è¯·éæ©è®¡åæ§è¡äººå" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | @change="handleExecutorChange" |
| | | > |
| | | <el-option |
| | | v-for="item in plannerOptions" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦è´¨æ£" prop="isQuality"> |
| | | <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> |
| | | </el-form-item> |
| | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance, onMounted } from "vue"; |
| | | import {add} from "@/api/productionManagement/productionProcess.js"; |
| | | import { modelListPage, productTreeList } from "@/api/basicData/product"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ |
| | | const formState = ref({ |
| | | name: '', |
| | | productId: undefined, |
| | | productModelId: undefined, |
| | | type: undefined, |
| | | remark: '', |
| | | salaryQuota: '', |
| | | plannerId: undefined, |
| | | plannerName: '', |
| | | executorId: undefined, |
| | | executorName: '', |
| | | isQuality: false, |
| | | }); |
| | | const productCategoryOptions = ref([]); |
| | | const modelOptions = ref([]); |
| | | const plannerOptions = ref([]); |
| | | |
| | | const isShow = computed({ |
| | |
| | | callback(); |
| | | }; |
| | | |
| | | const convertProductTree = list => { |
| | | return (list || []).map(item => { |
| | | const children = convertProductTree(item.children || item.childList || []); |
| | | return { |
| | | ...item, |
| | | value: item.id, |
| | | label: item.name || item.label, |
| | | children, |
| | | disabled: children.length > 0, |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | const findNodeById = (nodes, targetId) => { |
| | | for (const node of nodes || []) { |
| | | if (String(node.value) === String(targetId)) { |
| | | return node; |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeById(node.children, targetId); |
| | | if (found) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const getProductCategoryOptions = async () => { |
| | | try { |
| | | const res = await productTreeList(); |
| | | const list = Array.isArray(res) ? res : res?.data || []; |
| | | productCategoryOptions.value = convertProductTree(list); |
| | | } catch (e) { |
| | | productCategoryOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getModelOptions = async productId => { |
| | | if (!productId) { |
| | | modelOptions.value = []; |
| | | return; |
| | | } |
| | | try { |
| | | const res = await modelListPage({ |
| | | id: productId, |
| | | current: 1, |
| | | size: 999, |
| | | }); |
| | | const records = res?.records || res?.data?.records || []; |
| | | modelOptions.value = records; |
| | | } catch (e) { |
| | | modelOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const getPlannerOptions = async () => { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | |
| | | formState.value.plannerName = selectedUser?.nickName || ''; |
| | | }; |
| | | |
| | | const handleProductChange = async value => { |
| | | const selectedNode = findNodeById(productCategoryOptions.value, value); |
| | | formState.value.name = selectedNode?.label || ''; |
| | | formState.value.productModelId = undefined; |
| | | await getModelOptions(value); |
| | | const handleExecutorChange = value => { |
| | | const selectedUser = plannerOptions.value.find(item => String(item.userId) === String(value)); |
| | | formState.value.executorName = selectedUser?.nickName || ''; |
| | | }; |
| | | |
| | | const closeModal = () => { |
| | |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getProductCategoryOptions(); |
| | | getPlannerOptions(); |
| | | }); |
| | | |
| | |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | const tableColumn = ref([ |
| | | |
| | | { |
| | | label: "产ååç§°", |
| | | label: "é¨ä»¶åç§°", |
| | | prop: "name", |
| | | }, |
| | | { |
| | | label: "产åè§æ ¼", |
| | | prop: "productModel", |
| | | }, |
| | | { |
| | | label: "é¨ä»¶ç¼å·", |
| | |
| | | prop: "plannerName", |
| | | }, |
| | | { |
| | | label: "è®¡åæ§è¡äººå", |
| | | prop: "executorName", |
| | | }, |
| | | { |
| | | label: "æ¯å¦è´¨æ£", |
| | | prop: "isQuality", |
| | | formatData: (params) => { |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 审æ¹äººéæ©ï¼ä»¿åå审æ¹éç审æ¹äººèç¹éæ©ï¼ --> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <span>审æ¹äººéæ©ï¼</span> |
| | | <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">æ°å¢èç¹</el-button> |
| | | </template> |
| | | <div style="display: flex; align-items: flex-end; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | style="margin-right: 20px; text-align: center; margin-bottom: 10px;" |
| | | > |
| | | <div> |
| | | <span>审æ¹äºº</span> |
| | | â |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | filterable |
| | | style="width: 140px; margin-bottom: 8px;" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | }, |
| | | }); |
| | | const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); |
| | | |
| | | // å货审æ¹äººèç¹ï¼ä»¿ååå®¡æ¹ infoFormDia.vueï¼ |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | | let nextApproverId = 2; |
| | | const addApproverNode = () => { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }); |
| | | }; |
| | | const removeApproverNode = (index) => { |
| | | approverNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | // 导å
¥ç¸å
³ |
| | | const importUploadRef = ref(null); |
| | |
| | | deliveryForm.value = { |
| | | type: "货车", |
| | | }; |
| | | // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | deliveryFormVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const submitDelivery = () => { |
| | | proxy.$refs["deliveryFormRef"].validate((valid) => { |
| | | if (valid) { |
| | | // 审æ¹äººå¿
å¡«æ ¡éªï¼ææèç¹é½è¦éäººï¼ |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId); |
| | | if (hasEmptyApprover) { |
| | | proxy.$modal.msgError("请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼"); |
| | | return; |
| | | } |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | // ä¿åå½åå±å¼çè¡IDï¼ä»¥ä¾¿åè´§åéæ°å è½½åè¡¨æ ¼æ°æ® |
| | | const currentExpandedKeys = [...expandedRowKeys.value]; |
| | | const salesLedgerId = currentDeliveryRow.value.salesLedgerId; |
| | |
| | | salesLedgerId: salesLedgerId, |
| | | salesLedgerProductId: currentDeliveryRow.value.id, |
| | | type: deliveryForm.value.type, |
| | | approveUserIds, |
| | | }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("åè´§æå"); |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 审æ¹äººä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"> |
| | | <UserFilled/> |
| | | </el-icon> |
| | | <span class="card-title">审æ¹äººéæ©</span> |
| | | <el-button type="primary" size="small" @click="addApproverNode" class="header-btn"> |
| | | <el-icon> |
| | | <Plus/> |
| | | </el-icon> |
| | | æ°å¢èç¹ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <div class="form-content"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-label"> |
| | | <span class="node-step">{{ index + 1 }}</span> |
| | | <span class="node-text">审æ¹äºº</span> |
| | | <el-icon class="arrow-icon"> |
| | | <ArrowRight/> |
| | | </el-icon> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | class="approver-select" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | :icon="Delete" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | class="remove-btn" |
| | | >å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 产åä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | |
| | | <FormDialog v-model="importDialogVisible" title="导å
¥æ¥ä»·å" width="85%" :close-on-click-modal="false" |
| | | @close="importDialogVisible = false" @confirm="handleImportSubmit" |
| | | @cancel="importDialogVisible = false"> |
| | | <!-- 审æ¹äººä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"> |
| | | <UserFilled/> |
| | | </el-icon> |
| | | <span class="card-title">审æ¹äººéæ©</span> |
| | | <el-button type="primary" size="small" @click="addImportApproverNode" class="header-btn"> |
| | | <el-icon> |
| | | <Plus/> |
| | | </el-icon> |
| | | æ°å¢èç¹ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <div class="form-content"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in importApproverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-label"> |
| | | <span class="node-step">{{ index + 1 }}</span> |
| | | <span class="node-text">审æ¹äºº</span> |
| | | <el-icon class="arrow-icon"> |
| | | <ArrowRight/> |
| | | </el-icon> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="éæ©äººå" |
| | | class="approver-select" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | :icon="Delete" |
| | | @click="removeImportApproverNode(index)" |
| | | v-if="importApproverNodes.length > 1" |
| | | class="remove-btn" |
| | | >å é¤ |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | |
| | | const importDialogVisible = ref(false) |
| | | const viewDialogVisible = ref(false) |
| | | const importFileList = ref([]) |
| | | const importApproverNodes = ref([ |
| | | {id: 1, userId: null} |
| | | ]) |
| | | let nextImportApproverId = 2 |
| | | |
| | | const fileUpload = ref(null) |
| | | |
| | |
| | | const userList = ref([]); |
| | | const customerOption = ref([]); |
| | | |
| | | // 审æ¹äººèç¹ç¸å
³ |
| | | const approverNodes = ref([ |
| | | {id: 1, userId: null} |
| | | ]) |
| | | let nextApproverId = 2 |
| | | |
| | | const isEdit = ref(false) |
| | | const showDetail = ref(false) |
| | | const editId = ref(null) |
| | | const currentQuotation = ref({}) |
| | | const formRef = ref() |
| | | |
| | | // æ·»å 审æ¹äººèç¹ |
| | | function addApproverNode() { |
| | | approverNodes.value.push({id: nextApproverId++, userId: null}) |
| | | } |
| | | |
| | | // å é¤å®¡æ¹äººèç¹ |
| | | function removeApproverNode(index) { |
| | | approverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | // 导å
¥å¼¹çªå®¡æ¹äººèç¹ç¸å
³ |
| | | function addImportApproverNode() { |
| | | importApproverNodes.value.push({id: nextImportApproverId++, userId: null}) |
| | | } |
| | | |
| | | function removeImportApproverNode(index) { |
| | | importApproverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | function triggerRemoveImportFile(file) { |
| | | const index = importFileList.value.indexOf(file) |
| | |
| | | return |
| | | } |
| | | |
| | | const hasEmptyApprover = importApproverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | ElMessage.error('请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼') |
| | | return |
| | | } |
| | | |
| | | const selectedFile = importFileList.value[0] |
| | | const rawFile = selectedFile?.raw || selectedFile |
| | | if (!validateImportFile(rawFile)) { |
| | |
| | | |
| | | const formData = new FormData() |
| | | formData.append('file', rawFile) |
| | | |
| | | // å®¡æ ¸äºº IDsï¼ä»¥éå·åå² |
| | | const approveUserIds = importApproverNodes.value.map(node => node.userId).join(',') |
| | | formData.append('approveUserIdsJson', approveUserIds) |
| | | |
| | | loading.value = true |
| | | try { |
| | |
| | | const handleImport = async () => { |
| | | importFileList.value = [] |
| | | |
| | | // â
æ¸
空â导å
¥ç¨âç审æ¹äºº |
| | | importApproverNodes.value = [{id: 1, userId: null}] |
| | | nextImportApproverId = 2 |
| | | |
| | | let userLists = await userListNoPage(); |
| | | importDialogVisible.value = true |
| | | |
| | |
| | | dialogTitle.value = 'æ°å¢æ¥ä»·' |
| | | isEdit.value = false |
| | | resetForm() |
| | | // é置审æ¹äººèç¹ |
| | | approverNodes.value = [{id: 1, userId: null}] |
| | | nextApproverId = 2 |
| | | dialogVisible.value = true |
| | | let userLists = await userListNoPage(); |
| | | // åªå¤å¶éè¦çåæ®µï¼é¿å
å°ç»ä»¶å¼ç¨æ¾å
¥ååºå¼å¯¹è±¡ |
| | |
| | | form.discountAmount = row.discountAmount || 0 |
| | | form.totalAmount = row.totalAmount || 0 |
| | | |
| | | // 忾审æ¹äºº |
| | | if (row.approveUserIds) { |
| | | const userIds = row.approveUserIds.split(',') |
| | | approverNodes.value = userIds.map((userId, idx) => ({ |
| | | id: idx + 1, |
| | | userId: parseInt(userId.trim()) |
| | | })) |
| | | nextApproverId = userIds.length + 1 |
| | | } else { |
| | | approverNodes.value = [{id: 1, userId: null}] |
| | | nextApproverId = 2 |
| | | } |
| | | |
| | | // å è½½ç¨æ·å表 |
| | | let userLists = await userListNoPage(); |
| | | userList.value = (userLists.data || []).map(item => ({ |
| | |
| | | return |
| | | } |
| | | |
| | | // 审æ¹äººå¿
å¡«æ ¡éª |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | ElMessage.error('请为ææå®¡æ¹èç¹éæ©å®¡æ¹äººï¼') |
| | | return |
| | | } |
| | | |
| | | // æ¶éææèç¹ç审æ¹äººid |
| | | form.approveUserIds = approverNodes.value.map(node => node.userId).join(',') |
| | | |
| | | // è®¡ç®ææäº§åçåä»·æ»å |
| | | form.totalAmount = form.products.reduce((sum, product) => { |
| | | const price = Number(product.unitPrice) || 0 |
| | |
| | | validDate: item.validDate || '', |
| | | paymentMethod: item.paymentMethod || '', |
| | | status: item.status || 'è稿', |
| | | // 审æ¹äººï¼ç¨äºç¼è¾æ¶åæ¾ï¼ |
| | | approveUserIds: item.approveUserIds || '', |
| | | remark: item.remark || '', |
| | | products: item.products ? item.products.map(product => ({ |
| | | productId: product.productId || '', |