Pārlūkot izejas kodu

修复经营目标,新绩效系统管理记录文件上传,预览问题

manywhy 2 dienas atpakaļ
vecāks
revīzija
f168f6b4ae
100 mainītis faili ar 21724 papildinājumiem un 435 dzēšanām
  1. 401 0
      src/assets/css/base.css
  2. 105 0
      src/assets/css/override-element-ui.css
  3. 1145 0
      src/assets/css/workflow.css
  4. 539 0
      src/assets/iconfont/demo.css
  5. 1062 0
      src/assets/iconfont/demo_index.html
  6. 168 0
      src/assets/iconfont/iconfont.css
  7. 0 0
      src/assets/iconfont/iconfont.js
  8. 268 0
      src/assets/iconfont/iconfont.json
  9. BIN
      src/assets/iconfont/iconfont.ttf
  10. BIN
      src/assets/iconfont/iconfont.woff
  11. BIN
      src/assets/iconfont/iconfont.woff2
  12. BIN
      src/assets/images/add-close.png
  13. BIN
      src/assets/images/add-close1.png
  14. BIN
      src/assets/images/cancel.png
  15. BIN
      src/assets/images/check_box.png
  16. BIN
      src/assets/images/close-circle.png
  17. BIN
      src/assets/images/delete.png
  18. BIN
      src/assets/images/left-arrow.png
  19. BIN
      src/assets/images/minus-90-90.png
  20. BIN
      src/assets/images/plus-90-90.png
  21. BIN
      src/assets/images/right-arrow.png
  22. BIN
      src/assets/images/warning.png
  23. 14 0
      src/assets/theme.less
  24. 54 0
      src/components/common/Ellipsis.vue
  25. 464 0
      src/components/common/OrgPicker.vue
  26. 36 0
      src/components/common/Tip.vue
  27. 103 0
      src/components/common/WDialog.vue
  28. 189 0
      src/components/organization/BasicSetting.vue
  29. 48 0
      src/components/organization/CustomForm.vue
  30. 167 0
      src/components/organization/CustomProcess.vue
  31. 326 0
      src/components/organization/DragOrg-12-11.vue
  32. 557 0
      src/components/organization/DragOrg2Tree.vue
  33. 14 0
      src/components/organization/DynamicForm/formRender.vue
  34. 39 0
      src/components/organization/DynamicForm/index.vue
  35. 217 0
      src/components/organization/EmployeeTable copy 3.vue
  36. 557 0
      src/components/organization/EmployeeTable copy 4.vue
  37. 535 0
      src/components/organization/EmployeeTable copy 5.vue
  38. 557 0
      src/components/organization/EmployeeTable copy 6.vue
  39. 299 0
      src/components/organization/EmployeeTable copy.vue
  40. 1761 0
      src/components/organization/EmployeeTable-master.vue
  41. 240 0
      src/components/organization/EmployeeTable-useable.vue
  42. 168 0
      src/components/organization/Process/index.vue
  43. 157 0
      src/components/organization/Process/nodeWrap copy.vue
  44. 22 0
      src/components/organization/Process/nodeWrap.vue
  45. 172 0
      src/components/organization/workflow.vue
  46. 65 13
      src/newPerformance/components/MyPerformance.vue
  47. 233 0
      src/newPerformance/components/MyPerformance/AddRecords.vue
  48. 1 1
      src/newPerformance/components/MyPerformance/ExamineLog.vue
  49. 746 0
      src/newPerformance/components/MyPerformance/ManageRecord copy.vue
  50. 670 0
      src/newPerformance/components/MyPerformance/ManageRecord.vue
  51. 95 11
      src/newPerformance/components/ProcessTracking.vue
  52. 150 12
      src/newPerformance/components/PublicComp/TargetPlanComp.vue
  53. 2 0
      src/newPerformance/components/PublicComp/WorkFlow/index.vue
  54. 117 45
      src/newPerformance/components/Workbench/EditNode.vue
  55. 152 0
      src/newPerformance/components/Workbench/ShowManageRecords.vue
  56. 0 2
      src/newPerformance/views/Index.vue
  57. 3 1
      src/newPerformance/views/MoreNode.vue
  58. 7 8
      src/okr/views/okrIndex.vue
  59. 1656 0
      src/okr/views/targetBusiness/companyTargetList copy.vue
  60. 13 13
      src/okr/views/targetBusiness/companyTargetList.vue
  61. 0 4
      src/okr/views/targetBusiness/components/AddCycle.vue
  62. 8 7
      src/okr/views/targetBusiness/components/AddTarget.vue
  63. 394 0
      src/okr/views/targetBusiness/components/AppendFileUpload copy 2.vue
  64. 514 0
      src/okr/views/targetBusiness/components/AppendFileUpload copy 3.vue
  65. 275 0
      src/okr/views/targetBusiness/components/AppendFileUpload copy.vue
  66. 432 53
      src/okr/views/targetBusiness/components/AppendFileUpload.vue
  67. 0 1
      src/okr/views/targetBusiness/components/DataCenter.vue
  68. 7 5
      src/okr/views/targetBusiness/components/DataStatistics/StackedBarChart.vue
  69. 57 7
      src/okr/views/targetBusiness/components/MultiCycleTable.vue
  70. 5 4
      src/okr/views/targetBusiness/components/TargetDetail/EditBaseTableData.vue
  71. 4 7
      src/okr/views/targetBusiness/components/TargetDetail/EditTableData.vue
  72. 0 1
      src/okr/views/targetBusiness/components/TargetDetail/MessageList.vue
  73. 0 1
      src/okr/views/targetBusiness/components/TargetDetail/TargetSearchMulti.vue
  74. 0 3
      src/okr/views/targetBusiness/components/TargetDetail/TargetSearchSingle.vue
  75. 0 1
      src/okr/views/targetBusiness/components/TargetDetail/TaskList.vue
  76. 252 25
      src/okr/views/targetBusiness/components/TargetDetailComp.vue
  77. 1256 0
      src/okr/views/targetBusiness/components/previewFile.vue
  78. 0 1
      src/okr/views/targetBusiness/cycleList.vue
  79. 3 9
      src/okr/views/targetBusiness/dataStatistics.vue
  80. 11 5
      src/okr/views/targetBusiness/deptTargetList.vue
  81. 17 11
      src/okr/views/targetBusiness/personalTargetList.vue
  82. 17 6
      src/okr/views/targetBusiness/previewFile.vue
  83. 1759 0
      src/okr/views/targetBusiness/targetDetail copy.vue
  84. 406 176
      src/okr/views/targetBusiness/targetDetail.vue
  85. 5 2
      src/okr/views/targetBusiness/utils/step/targetDetailSteps.js
  86. 54 0
      src/store/modules/flowDesigner.js
  87. 236 0
      src/store/modules/workflow.js
  88. 76 0
      src/utils/api/design.js
  89. 32 0
      src/utils/api/org.js
  90. 0 0
      src/utils/api/process.js
  91. 65 0
      src/utils/api/request.js
  92. 43 0
      src/utils/findDeptItem.js
  93. 72 0
      src/utils/workflow/const.js
  94. 58 0
      src/utils/workflow/formatFlowPreview.js
  95. 172 0
      src/utils/workflow/formatcommit_data.js
  96. 107 0
      src/utils/workflow/formatdisplay_data.js
  97. 237 0
      src/utils/workflow/hsharpUtils.js
  98. 176 0
      src/utils/workflow/index.js
  99. 32 0
      src/utils/workflow/loading.js
  100. 648 0
      src/utils/workflow/nodeUtils.js

+ 401 - 0
src/assets/css/base.css

@@ -0,0 +1,401 @@
+body {
+  margin: 0;
+  font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI,
+    PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica,
+    Arial, sans-serif;
+  font-size: 14px;
+  font-variant: tabular-nums;
+  line-height: 1.5;
+  color: rgba(0, 0, 0, 0.65);
+  background-color: #fff;
+}
+
+body,
+html {
+  font-family: sans-serif;
+  line-height: 1.15;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+  -ms-overflow-style: scrollbar;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+input::-ms-clear,
+input::-ms-reveal {
+  display: none;
+}
+
+*,
+:after,
+:before {
+  box-sizing: border-box;
+}
+
+@-ms-viewport {
+  width: device-width;
+}
+
+article,
+aside,
+dialog,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section {
+  display: block;
+}
+
+[tabindex="-1"]:focus {
+  outline: none !important;
+}
+
+hr {
+  box-sizing: content-box;
+  height: 0;
+  overflow: visible;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  margin-top: 0;
+  margin-bottom: 0.5em;
+  color: rgba(0, 0, 0, 0.85);
+  font-weight: 500;
+}
+
+p {
+  margin-top: 0;
+  margin-bottom: 1em;
+}
+
+abbr[data-original-title],
+abbr[title] {
+  text-decoration: underline;
+  text-decoration: underline dotted;
+  cursor: help;
+  border-bottom: 0;
+}
+
+address {
+  margin-bottom: 1em;
+  font-style: normal;
+  line-height: inherit;
+}
+
+input[type="number"],
+input[type="password"],
+input[type="text"],
+textarea {
+  appearance: none;
+  -webkit-appearance: none;
+}
+
+dl,
+ol,
+ul {
+  margin-top: 0;
+  margin-bottom: 1em;
+}
+
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+  margin-bottom: 0;
+}
+
+dt {
+  font-weight: 500;
+}
+
+dd {
+  margin-bottom: 0.5em;
+  margin-left: 0;
+}
+
+blockquote {
+  margin: 0 0 1em;
+}
+
+dfn {
+  font-style: italic;
+}
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+small {
+  font-size: 80%;
+}
+
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+a {
+  color: #1890ff;
+  background-color: transparent;
+  text-decoration: none;
+  outline: none;
+  cursor: pointer;
+  transition: color 0.3s;
+  -webkit-text-decoration-skip: objects;
+}
+
+a:focus {
+  text-decoration: underline;
+  text-decoration-skip: auto;
+}
+
+a:hover {
+  color: #40a9ff;
+}
+
+a:active {
+  color: #096dd9;
+}
+
+a:active,
+a:hover {
+  outline: 0;
+  text-decoration: none;
+}
+
+code,
+kbd,
+pre,
+samp {
+  font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier,
+    monospace;
+  font-size: 1em;
+}
+
+pre {
+  margin-top: 0;
+  margin-bottom: 1em;
+  overflow: auto;
+}
+
+figure {
+  margin: 0 0 1em;
+}
+
+img {
+  vertical-align: middle;
+  border-style: none;
+}
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+[role="button"],
+a,
+area,
+button,
+input:not([type="range"]),
+label,
+select,
+summary,
+textarea {
+  touch-action: manipulation;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+caption {
+  padding-top: 0.75em;
+  padding-bottom: 0.3em;
+  color: rgba(0, 0, 0, 0.45);
+  text-align: left;
+  caption-side: bottom;
+}
+
+th {
+  text-align: inherit;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  /* font-size: inherit; */
+  line-height: inherit;
+  color: inherit;
+}
+
+button,
+input {
+  overflow: visible;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+[type="reset"],
+[type="submit"],
+button,
+html [type="button"] {
+  appearance: button;
+  -webkit-appearance: button;
+}
+
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner,
+button::-moz-focus-inner {
+  padding: 0;
+  border-style: none;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+  box-sizing: border-box;
+  padding: 0;
+}
+
+input[type="date"],
+input[type="datetime-local"],
+input[type="month"],
+input[type="time"] {
+  appearance: listbox;
+  -webkit-appearance: listbox;
+}
+
+textarea {
+  overflow: auto;
+  resize: vertical;
+}
+
+fieldset {
+  min-width: 0;
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  max-width: 100%;
+  padding: 0;
+  margin-bottom: 0.5em;
+  font-size: 1.5em;
+  line-height: inherit;
+  color: inherit;
+  white-space: normal;
+}
+
+progress {
+  vertical-align: baseline;
+}
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+[type="search"] {
+  outline-offset: -2px;
+  appearance: none;
+  -webkit-appearance: none;
+}
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+::-webkit-file-upload-button {
+  font: inherit;
+  -webkit-appearance: button;
+}
+
+output {
+  display: inline-block;
+}
+
+summary {
+  display: list-item;
+}
+
+template {
+  display: none;
+}
+
+[hidden] {
+  display: none !important;
+}
+
+mark {
+  padding: 0.2em;
+  background-color: #feffe6;
+}
+
+::selection {
+  background: #1890ff;
+  color: #fff;
+}
+
+.clearfix {
+  zoom: 1;
+}
+
+.clearfix:after,
+.clearfix:before {
+  content: "";
+  display: table;
+}
+
+.clearfix:after {
+  clear: both;
+}
+
+.wrap {
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  height: 100%;
+}
+
+.iconfont {
+  font-family: IconFont !important;
+  font-size: 33px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -webkit-text-stroke-width: 0.2px;
+  -moz-osx-font-smoothing: grayscale;
+}

+ 105 - 0
src/assets/css/override-element-ui.css

@@ -0,0 +1,105 @@
+.el-drawer__header {
+  margin-bottom: 0 !important;
+  padding: 14px 0 14px 20px !important;
+  color: #323232 !important;
+  font-size: 16px !important;
+}
+.el-drawer__header .el-drawer__title {
+  font-size: 16px !important;
+}
+.drawer_content {
+  display: flex !important;
+  flex-direction: column !important;
+  height: 100% !important;
+}
+.drawer_content > div {
+  border-top: 1px solid #f2f2f2 !important;
+}
+.flow-drawer__footer {
+  padding: 10px 30px !important;
+  border-top: 1px solid #f2f2f2 !important;
+}
+
+.flow-drawer__footer .el-button {
+  float: right !important;
+  margin-right: 10px !important;
+}
+
+.el-dialog {
+  width: 520px;
+  border: 1px solid #dde1e5 !important;
+  border-radius: 3px !important;
+}
+
+.el-dialog__header {
+  padding: 0 0 0 20px !important;
+  line-height: 50px !important;
+  height: 50px !important;
+  background: #fff !important;
+  border-bottom: 1px solid #f2f2f2 !important;
+}
+
+.el-dialog__header .el-dialog__title {
+  font-size: 16px !important;
+  line-height: 50px !important;
+  color: #333333 !important;
+}
+
+.el-dialog__header .el-dialog__headerbtn {
+  height: 12px !important;
+  width: 12px !important;
+}
+
+.el-dialog__header .el-icon-close {
+  width: 12px !important;
+  height: 12px !important;
+  float: left !important;
+}
+
+.el-dialog__header .el-icon-close::before {
+  display: block !important;
+  width: 12px !important;
+  height: 12px !important;
+  background: url(~@/assets/images/add-close.png) no-repeat center !important;
+  background-size: 100% 100% !important;
+  content: "" !important;
+}
+.el-drawer__body {
+  padding: 0 !important;
+}
+.el-dialog__footer {
+  border-top: 1px solid #f2f2f2 !important;
+  padding-bottom: 10px !important;
+}
+
+.el-checkbox,
+.el-checkbox__input.is-checked + .el-checkbox__label,
+.el-radio,
+.el-radio__input.is-checked + .el-radio__label,
+.el-dialog__body,
+.el-tree {
+  color: #333 !important;
+}
+
+.el-radio__label,
+.el-checkbox__label {
+  font-size: 12px !important;
+}
+
+.el-timeline {
+  --el-timeline-node-size-normal: 25px !important;
+  --el-timeline-node-size-large: 25px !important;
+}
+.el-timeline-item__node--normal {
+  left: -8px !important;
+}
+.el-timeline-item__node--large {
+  left: -8px !important;
+}
+.el-timeline-item__wrapper {
+  top: -10px !important;
+}
+.el-tabs--border-card {
+  min-width: 800px !important;
+  min-height: 550px !important;
+}

+ 1145 - 0
src/assets/css/workflow.css

@@ -0,0 +1,1145 @@
+.filter-container {
+  padding: 5px 0 10px 0;
+}
+
+.filter-container .el-button,
+.filter-container .el-input,
+.filter-container .el-input__inner {
+  padding: 0 15px;
+  height: 34px;
+  line-height: 34px;
+}
+
+.form-container {
+  margin: auto;
+  background: #fff !important;
+  padding: 30px 15px 15px 15px;
+  text-align: center;
+  max-width: 800px;
+  height: 100%;
+}
+
+.page-close-box {
+  position: absolute;
+  top: -0;
+  right: -0;
+  width: 42px;
+  height: 42px;
+  text-align: center;
+  cursor: pointer;
+  z-index: 999;
+}
+
+.page-close-box img {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  background-image: url();
+}
+
+.page-close-box::before {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 0;
+  height: 0;
+  border: 20px solid #46a6fe;
+  border-color: #46a6fe #46a6fe transparent transparent;
+  content: "";
+}
+
+.flowicon {
+  display: inline-block;
+  font-style: normal;
+  vertical-align: baseline;
+  text-align: center;
+  text-transform: none;
+  line-height: 1;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.flowicon:before {
+  display: block;
+  font-family: flow !important;
+}
+
+.flowicon-close:before {
+  content: url("../images/add-close.png");
+  filter: brightness(0) saturate(100%) invert(0);
+}
+.flowicon-close-circle:before {
+  content: url("../images/cancel.png");
+}
+
+.flowicon-delete:before {
+  content: url("../images/delete.png");
+}
+
+.flowicon-right:before {
+  content: url("../images/right-arrow.png");
+  filter: brightness(0) saturate(100%) invert(0);
+}
+
+.flowicon-left:before {
+  content: url("../images/left-arrow.png");
+}
+.flowicon-exclamation-circle:before {
+  content: url("../images/warning.png");
+}
+.flow-btn {
+  line-height: 1.5;
+  display: inline-block;
+  font-weight: 400;
+  text-align: center;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+  white-space: nowrap;
+  padding: 0 15px;
+  font-size: 14px;
+  border-radius: 4px;
+  height: 32px;
+  user-select: none;
+  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  position: relative;
+  color: rgba(0, 0, 0, 0.65);
+  background-color: #fff;
+  border-color: #d9d9d9;
+}
+
+.flow-btn > .flowicon {
+  line-height: 1;
+}
+
+.flow-btn,
+.flow-btn:active,
+.flow-btn:focus {
+  outline: 0;
+}
+
+.flow-btn > a:only-child {
+  color: currentColor;
+}
+
+.flow-btn > a:only-child:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: transparent;
+}
+
+.flow-btn:focus,
+.flow-btn:hover {
+  color: #40a9ff;
+  background-color: #fff;
+  border-color: #40a9ff;
+}
+
+.flow-btn:focus > a:only-child,
+.flow-btn:hover > a:only-child {
+  color: currentColor;
+}
+
+.flow-btn:focus > a:only-child:after,
+.flow-btn:hover > a:only-child:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: transparent;
+}
+
+.flow-btn.active,
+.flow-btn:active {
+  color: #096dd9;
+  background-color: #fff;
+  border-color: #096dd9;
+}
+
+.flow-btn.active > a:only-child,
+.flow-btn:active > a:only-child {
+  color: currentColor;
+}
+
+.flow-btn.active > a:only-child:after,
+.flow-btn:active > a:only-child:after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: transparent;
+}
+
+.flow-btn.active,
+.flow-btn:active,
+.flow-btn:focus,
+.flow-btn:hover {
+  background: #fff;
+  text-decoration: none;
+}
+
+.flow-btn > i,
+.flow-btn > span {
+  pointer-events: none;
+}
+
+.flow-btn:before {
+  position: absolute;
+  top: -1px;
+  left: -1px;
+  bottom: -1px;
+  right: -1px;
+  background: #fff;
+  opacity: 0.35;
+  content: "";
+  border-radius: inherit;
+  z-index: 1;
+  transition: opacity 0.2s;
+  pointer-events: none;
+  display: none;
+}
+
+.flow-btn .flowicon {
+  transition: margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.flow-btn:active > span,
+.flow-btn:focus > span {
+  position: relative;
+}
+
+.flow-btn > .flowicon + span,
+.flow-btn > span + .flowicon {
+  margin-left: 8px;
+}
+
+.my-nav-container {
+  display: inline-block;
+  position: relative;
+}
+
+.my-nav-container .ghost-bar {
+  position: absolute;
+  width: 150px;
+  height: 100%;
+  left: 0;
+  background: #1583f2;
+  -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.my-nav-container .ghost-bar:after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-width: 0 5px 6px;
+  border-color: transparent transparent #f6f6f6;
+}
+
+.my-nav-item {
+  position: relative;
+  cursor: pointer;
+  display: inline-block;
+  line-height: 60px;
+  width: 150px;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.my-nav-item .order-num {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  line-height: 20px;
+  border: 1px solid #fff;
+  border-radius: 50%;
+  margin-right: 6px;
+  -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.my-nav-item.active .order-num {
+  color: #1583f2;
+  background: #fff;
+}
+
+.flow-input {
+  font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI,
+    PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica,
+    Arial, sans-serif;
+  font-variant: tabular-nums;
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+  position: relative;
+  display: inline-block;
+  padding: 4px 11px;
+  width: 100%;
+  height: 32px;
+  font-size: 14px;
+  line-height: 1.5;
+  color: rgba(0, 0, 0, 0.65);
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  transition: all 0.3s;
+}
+
+.flow-input::-moz-placeholder {
+  color: #bfbfbf;
+  opacity: 1;
+}
+
+.flow-input:-ms-input-placeholder {
+  color: #bfbfbf;
+}
+
+.flow-input::-webkit-input-placeholder {
+  color: #bfbfbf;
+}
+
+.flow-input:focus,
+.flow-input:hover {
+  border-color: #40a9ff;
+  border-right-width: 1px !important;
+}
+
+.flow-input:focus {
+  outline: 0;
+  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+textarea.flow-input {
+  max-width: 100%;
+  height: auto;
+  vertical-align: bottom;
+  transition: all 0.3s, height 0s;
+  min-height: 32px;
+}
+
+.my-nav {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 997;
+  width: 100%;
+  height: 60px;
+  font-size: 14px;
+  color: #fff;
+  background: #3296fa;
+  display: flex;
+  align-items: center;
+}
+
+.my-nav > * {
+  flex: 1;
+  width: 100%;
+}
+
+.my-nav .my-nav-left {
+  display: -webkit-box;
+  display: flex;
+  align-items: center;
+}
+
+.my-nav .my-nav-center {
+  flex: none;
+  width: 600px;
+  text-align: center;
+}
+
+.my-nav .my-nav-right {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  text-align: right;
+}
+
+.my-nav .my-nav-back {
+  display: inline-block;
+  width: 60px;
+  height: 60px;
+  font-size: 22px;
+  border-right: 1px solid #1583f2;
+  text-align: center;
+  cursor: pointer;
+}
+
+.my-nav .my-nav-back:hover {
+  background: #5af;
+}
+
+.my-nav .my-nav-back:active {
+  background: #1583f2;
+}
+
+.my-nav .my-nav-back .flowicon {
+  line-height: 60px;
+}
+
+.my-nav .my-nav-title {
+  width: 0;
+  flex: 1;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  padding: 0 15px;
+}
+
+.my-nav a {
+  color: #fff;
+  margin-left: 12px;
+}
+
+.my-nav .button-publish {
+  min-width: 80px;
+  margin-left: 4px;
+  margin-right: 15px;
+  color: #3296fa;
+  border-color: #fff;
+}
+
+.my-nav .button-publish.flow-btn:focus,
+.my-nav .button-publish.flow-btn:hover {
+  color: #3296fa;
+  border-color: #fff;
+  box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.3);
+}
+
+.my-nav .button-publish.flow-btn:active {
+  color: #3296fa;
+  background: #d6eaff;
+  box-shadow: none;
+}
+
+.my-nav .button-preview {
+  min-width: 80px;
+  margin-left: 16px;
+  margin-right: 4px;
+  color: #fff;
+  border-color: #fff;
+  background: transparent;
+}
+
+.my-nav .button-preview.flow-btn:focus,
+.my-nav .button-preview.flow-btn:hover {
+  color: #fff;
+  border-color: #fff;
+  background: #59acfc;
+}
+
+.my-nav .button-preview.flow-btn:active {
+  color: #fff;
+  border-color: #fff;
+  background: #2186ef;
+}
+
+.my-nav-content {
+  position: fixed;
+  top: 60px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 1;
+  overflow-x: hidden;
+  overflow-y: hidden;
+  padding-bottom: 30px;
+  height: 100%;
+}
+
+.error-modal-desc {
+  font-size: 13px;
+  color: red;
+  line-height: 22px;
+  margin-bottom: 14px;
+}
+
+.error-modal-list {
+  height: 200px;
+  overflow-y: auto;
+  margin-right: -25px;
+  padding-right: 25px;
+}
+
+.error-modal-item {
+  padding: 10px 20px;
+  line-height: 21px;
+  background: #f6f6f6;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+  border-radius: 4px;
+}
+
+.error-modal-item-label {
+  flex: none;
+  font-size: 15px;
+  color: rgba(25, 31, 37, 0.56);
+  padding-right: 10px;
+}
+
+.error-modal-item-content {
+  text-align: right;
+  flex: 1;
+  font-size: 13px;
+  color: red;
+}
+
+#body.blur {
+  -webkit-filter: blur(3px);
+  filter: blur(3px);
+}
+
+.zoom {
+  display: flex;
+  position: fixed;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -webkit-box-pack: justify;
+  -ms-flex-pack: justify;
+  justify-content: space-between;
+  height: 40px;
+  /*width: 125px;*/
+  right: 40px;
+  margin-top: 30px;
+  z-index: 10;
+}
+
+.zoom .zoom-in,
+.zoom .zoom-out {
+  width: 30px;
+  height: 30px;
+  background: #fff;
+  color: #c1c1cd;
+  cursor: pointer;
+  background-size: 100%;
+  background-repeat: no-repeat;
+}
+
+.zoom .zoom-out {
+  background-image: url(../images/minus-90-90.png);
+}
+
+.zoom .zoom-out.disabled {
+  opacity: 0.5;
+}
+
+.zoom .zoom-in {
+  background-image: url(../images/plus-90-90.png);
+}
+
+.zoom .zoom-in.disabled {
+  opacity: 0.5;
+}
+
+.zoom-reset {
+  color: #595d62;
+  font-weight: bold;
+  padding: 2px 7px;
+  background: #ffffff;
+  border: 1px solid #e3e4e4;
+  cursor: pointer;
+  margin-left: 10px;
+}
+
+.zoom > span {
+  text-align: center;
+  width: 39px;
+  margin: 0 10px;
+}
+
+.auto-judge:hover .editable-title,
+.node-wrap-box:hover .editable-title {
+  border-bottom: 1px dashed #fff;
+}
+
+.auto-judge:hover .editable-title.editing,
+.node-wrap-box:hover .editable-title.editing {
+  text-decoration: none;
+  border: 1px solid #d9d9d9;
+}
+
+.auto-judge:hover .editable-title {
+  border-color: #15bc83;
+}
+
+.editable-title {
+  line-height: 15px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  border-bottom: 1px dashed transparent;
+}
+
+.editable-title:before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 40px;
+}
+
+.editable-title:hover {
+  border-bottom: 1px dashed #fff;
+}
+
+.editable-title-input {
+  flex: none;
+  height: 18px;
+  padding-left: 4px;
+  text-indent: 0;
+  font-size: 12px;
+  line-height: 18px;
+  z-index: 1;
+}
+
+.editable-title-input:hover {
+  text-decoration: none;
+}
+
+.flow-btn {
+  position: relative;
+}
+
+.node-wrap-box {
+  display: -webkit-inline-box;
+  display: -ms-inline-flexbox;
+  display: inline-flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  position: relative;
+  width: 220px;
+  min-height: 72px;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  background: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.node-wrap-box:after {
+  pointer-events: none;
+  content: "";
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 2;
+  border-radius: 4px;
+  border: 1px solid transparent;
+  transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
+}
+
+.node-wrap-box.active:after,
+.node-wrap-box:active:after,
+.node-wrap-box:hover:after {
+  border: 1px solid #3296fa;
+  box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
+}
+
+.node-wrap-box.active .close,
+.node-wrap-box:active .close,
+.node-wrap-box:hover .close {
+  display: block;
+}
+
+.node-wrap-box.error:after {
+  border: 1px solid #f25643;
+  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
+}
+
+.node-wrap-box .title {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding-left: 16px;
+  padding-right: 30px;
+  width: 100%;
+  height: 24px;
+  line-height: 24px;
+  font-size: 12px;
+  color: #fff;
+  text-align: left;
+  background: #576a95;
+  border-radius: 4px 4px 0 0;
+}
+
+.node-wrap-box .title .iconfont {
+  font-size: 12px;
+  margin-right: 5px;
+}
+
+.node-wrap-box .placeholder {
+  color: #bfbfbf;
+}
+
+.node-wrap-box .close {
+  display: none;
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 20px;
+  height: 20px;
+  font-size: 14px;
+  color: #fff;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 20px;
+}
+
+.node-wrap-box .content {
+  position: relative;
+  font-size: 14px;
+  padding: 16px;
+  padding-right: 30px;
+}
+
+.node-wrap-box .content .text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  line-clamp: 3;
+  -webkit-box-orient: vertical;
+}
+
+.node-wrap-box .content .arrow {
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 20px;
+  height: 14px;
+  font-size: 14px;
+  color: #979797;
+}
+
+.start-node.node-wrap-box .content .text {
+  display: block;
+  white-space: nowrap;
+}
+
+.node-wrap-box:before {
+  content: "";
+  position: absolute;
+  top: -12px;
+  left: 50%;
+  -webkit-transform: translateX(-50%);
+  transform: translateX(-50%);
+  width: 0;
+  height: 4px;
+  border-style: solid;
+  border-width: 8px 6px 4px;
+  border-color: #cacaca transparent transparent;
+  background: #f5f5f7;
+}
+
+.node-wrap-box.start-node:before {
+  content: none;
+}
+
+.top-left-cover-line {
+  left: -1px;
+}
+
+.top-left-cover-line,
+.top-right-cover-line {
+  position: absolute;
+  height: 8px;
+  width: 50%;
+  background-color: #f5f5f7;
+  top: -4px;
+}
+
+.top-right-cover-line {
+  right: -1px;
+}
+
+.bottom-left-cover-line {
+  left: -1px;
+}
+
+.bottom-left-cover-line,
+.bottom-right-cover-line {
+  position: absolute;
+  height: 8px;
+  width: 50%;
+  background-color: #f5f5f7;
+  bottom: -4px;
+}
+
+.bottom-right-cover-line {
+  right: -1px;
+}
+
+.dingflow-design {
+  width: 100%;
+  background-color: #f5f5f7;
+  overflow: hidden;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  top: 0;
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.dingflow-design .box-scale {
+  transform: scale(1);
+  display: inline-block;
+  position: relative;
+  width: 100%;
+  padding: 54.5px 0;
+  -webkit-box-align: start;
+  -ms-flex-align: start;
+  align-items: flex-start;
+  -webkit-box-pack: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+  min-width: -webkit-min-content;
+  min-width: -moz-min-content;
+  min-width: min-content;
+  background-color: #f5f5f7;
+  /*transform-origin: 50% 0px 0px;*/
+}
+
+.dingflow-design .node-wrap {
+  flex-direction: column;
+  -webkit-box-pack: start;
+  -ms-flex-pack: start;
+  justify-content: flex-start;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+  -webkit-box-flex: 1;
+  -ms-flex-positive: 1;
+  padding: 0 50px;
+  position: relative;
+  z-index: 9999;
+}
+
+.dingflow-design .branch-wrap,
+.dingflow-design .node-wrap {
+  display: inline-flex;
+  width: 100%;
+}
+
+.dingflow-design .branch-box-wrap {
+  display: flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  min-height: 270px;
+  width: 100%;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+}
+
+.dingflow-design .branch-box {
+  display: flex;
+  overflow: visible;
+  min-height: 180px;
+  height: auto;
+  border-bottom: 2px solid #ccc;
+  border-top: 2px solid #ccc;
+  position: relative;
+  margin-top: 15px;
+}
+
+.dingflow-design .branch-box .col-box {
+  background: #f5f5f7;
+}
+
+.dingflow-design .branch-box .col-box:before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 0;
+  margin: auto;
+  width: 2px;
+  height: 100%;
+  background-color: #cacaca;
+}
+
+.dingflow-design .add-branch {
+  border: none;
+  outline: none;
+  user-select: none;
+  justify-content: center;
+  font-size: 12px;
+  padding: 0 10px;
+  height: 30px;
+  line-height: 30px;
+  border-radius: 15px;
+  color: #3296fa;
+  background: #fff;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+  position: absolute;
+  top: -16px;
+  left: 50%;
+  transform: translateX(-50%);
+  transform-origin: center center;
+  cursor: pointer;
+  z-index: 1;
+  display: inline-flex;
+  align-items: center;
+  -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.dingflow-design .add-branch:hover {
+  transform: translateX(-50%) scale(1.1);
+  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.1);
+}
+
+.dingflow-design .add-branch:active {
+  transform: translateX(-50%);
+  box-shadow: none;
+}
+
+.dingflow-design .col-box {
+  display: inline-flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+  flex-direction: column;
+  -webkit-box-align: center;
+  align-items: center;
+  position: relative;
+}
+
+.dingflow-design .condition-node {
+  min-height: 220px;
+}
+
+.dingflow-design .condition-node,
+.dingflow-design .condition-node-box {
+  display: inline-flex;
+  -webkit-box-orient: vertical;
+  -webkit-box-direction: normal;
+  flex-direction: column;
+  -webkit-box-flex: 1;
+}
+
+.dingflow-design .condition-node-box {
+  padding-top: 30px;
+  padding-right: 50px;
+  padding-left: 50px;
+  -webkit-box-pack: center;
+  justify-content: center;
+  -webkit-box-align: center;
+  align-items: center;
+  flex-grow: 1;
+  position: relative;
+}
+
+.dingflow-design .condition-node-box:before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  margin: auto;
+  width: 2px;
+  height: 100%;
+  background-color: #cacaca;
+}
+
+.dingflow-design .auto-judge {
+  position: relative;
+  width: 220px;
+  min-height: 72px;
+  background: #fff;
+  border-radius: 4px;
+  padding: 14px 19px;
+  cursor: pointer;
+}
+
+.dingflow-design .auto-judge:after {
+  pointer-events: none;
+  content: "";
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 2;
+  border-radius: 4px;
+  border: 1px solid transparent;
+  transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
+}
+
+.dingflow-design .auto-judge.active:after,
+.dingflow-design .auto-judge:active:after,
+.dingflow-design .auto-judge:hover:after {
+  border: 1px solid #3296fa;
+  box-shadow: 0 0 6px 0 rgba(50, 150, 250, 0.3);
+}
+
+.dingflow-design .auto-judge.active .close,
+.dingflow-design .auto-judge:active .close,
+.dingflow-design .auto-judge:hover .close {
+  display: block;
+}
+
+.dingflow-design .auto-judge.error:after {
+  border: 1px solid #f25643;
+  box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
+}
+
+.dingflow-design .auto-judge .title-wrapper {
+  position: relative;
+  font-size: 12px;
+  color: #15bc83;
+  text-align: left;
+  line-height: 16px;
+}
+
+.dingflow-design .auto-judge .title-wrapper .editable-title {
+  display: inline-block;
+  max-width: 120px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.dingflow-design .auto-judge .title-wrapper .priority-title {
+  float: right;
+  margin-right: 10px;
+  color: rgba(25, 31, 37, 0.56);
+}
+
+.dingflow-design .auto-judge .placeholder {
+  color: #bfbfbf;
+}
+
+.dingflow-design .auto-judge .close {
+  display: none;
+  position: absolute;
+  right: -10px;
+  top: -10px;
+  width: 20px;
+  height: 20px;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.25);
+  border-radius: 50%;
+  text-align: center;
+  line-height: 20px;
+  z-index: 2;
+}
+
+.dingflow-design .auto-judge .content {
+  font-size: 14px;
+  color: #191f25;
+  text-align: left;
+  margin-top: 6px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  line-clamp: 3;
+  -webkit-box-orient: vertical;
+}
+
+.dingflow-design .auto-judge .sort-left,
+.dingflow-design .auto-judge .sort-right {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  display: none;
+  z-index: 1;
+}
+
+.dingflow-design .auto-judge .sort-left {
+  left: 0;
+  border-right: 1px solid #f6f6f6;
+}
+
+.dingflow-design .auto-judge .sort-right {
+  right: 0;
+  border-left: 1px solid #f6f6f6;
+}
+
+.dingflow-design .auto-judge:hover .sort-left,
+.dingflow-design .auto-judge:hover .sort-right {
+  display: flex;
+  align-items: center;
+}
+
+.dingflow-design .auto-judge .sort-left:hover,
+.dingflow-design .auto-judge .sort-right:hover {
+  background: #efefef;
+}
+
+.dingflow-design .end-node {
+  border-radius: 50%;
+  font-size: 14px;
+  color: rgba(25, 31, 37, 0.4);
+  text-align: left;
+}
+
+.dingflow-design .end-node .end-node-circle {
+  width: 20px;
+  height: 20px;
+  margin: auto;
+  border-radius: 50%;
+  background: #dbdcdc;
+}
+
+.dingflow-design .end-node .end-node-text {
+  margin-top: 5px;
+  text-align: center;
+}
+
+.approval-setting {
+  border-radius: 2px;
+  margin: 20px 0;
+  position: relative;
+  background: #fff;
+}
+
+.flow-btn {
+  position: relative;
+}

+ 539 - 0
src/assets/iconfont/demo.css

@@ -0,0 +1,539 @@
+/* Logo 字体 */
+@font-face {
+  font-family: "iconfont logo";
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: "iconfont logo";
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown>p,
+.markdown>blockquote,
+.markdown>.highlight,
+.markdown>ol,
+.markdown>ul {
+  width: 80%;
+}
+
+.markdown ul>li {
+  list-style: circle;
+}
+
+.markdown>ul li,
+.markdown blockquote ul>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown>ul li p,
+.markdown>ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol>li {
+  list-style: decimal;
+}
+
+.markdown>ol li,
+.markdown blockquote ol>li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown>table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown>table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown>table th,
+.markdown>table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown>table th {
+  background: #F7F7F7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown>br,
+.markdown>p>br {
+  clear: both;
+}
+
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*="language-"],
+pre[class*="language-"] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection,
+pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection,
+code[class*="language-"] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection,
+pre[class*="language-"] ::selection,
+code[class*="language-"]::selection,
+code[class*="language-"] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+
+  code[class*="language-"],
+  pre[class*="language-"] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+  padding: 1em;
+  margin: .5em 0;
+  overflow: auto;
+}
+
+:not(pre)>code[class*="language-"],
+pre[class*="language-"] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre)>code[class*="language-"] {
+  padding: .1em;
+  border-radius: .3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

+ 1062 - 0
src/assets/iconfont/demo_index.html

@@ -0,0 +1,1062 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <title>iconfont Demo</title>
+  <link rel="shortcut icon" href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico" type="image/x-icon"/>
+  <link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg"/>
+  <link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
+  <link rel="stylesheet" href="demo.css">
+  <link rel="stylesheet" href="iconfont.css">
+  <script src="iconfont.js"></script>
+  <!-- jQuery -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
+  <!-- 代码高亮 -->
+  <script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
+  <style>
+    .main .logo {
+      margin-top: 0;
+      height: auto;
+    }
+
+    .main .logo a {
+      display: flex;
+      align-items: center;
+    }
+
+    .main .logo .sub-title {
+      margin-left: 0.5em;
+      font-size: 22px;
+      color: #fff;
+      background: linear-gradient(-45deg, #3967FF, #B500FE);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+    }
+  </style>
+</head>
+<body>
+  <div class="main">
+    <h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
+      <img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
+      
+    </a></h1>
+    <div class="nav-tabs">
+      <ul id="tabs" class="dib-box">
+        <li class="dib active"><span>Unicode</span></li>
+        <li class="dib"><span>Font class</span></li>
+        <li class="dib"><span>Symbol</span></li>
+      </ul>
+      
+      <a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=3538338" target="_blank" class="nav-more">查看项目</a>
+      
+    </div>
+    <div class="tab-container">
+      <div class="content unicode" style="display: block;">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe61c;</span>
+                <div class="name">iconfont-kefu</div>
+                <div class="code-name">&amp;#xe61c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe648;</span>
+                <div class="name">BBD密码</div>
+                <div class="code-name">&amp;#xe648;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe636;</span>
+                <div class="name">人力社保</div>
+                <div class="code-name">&amp;#xe636;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe758;</span>
+                <div class="name">部门</div>
+                <div class="code-name">&amp;#xe758;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xec7f;</span>
+                <div class="name">插入图片</div>
+                <div class="code-name">&amp;#xec7f;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe610;</span>
+                <div class="name">考勤管理</div>
+                <div class="code-name">&amp;#xe610;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe614;</span>
+                <div class="name">身份证</div>
+                <div class="code-name">&amp;#xe614;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe64b;</span>
+                <div class="name">位置</div>
+                <div class="code-name">&amp;#xe64b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe966;</span>
+                <div class="name">24gf-phoneBubble</div>
+                <div class="code-name">&amp;#xe966;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe643;</span>
+                <div class="name">考勤</div>
+                <div class="code-name">&amp;#xe643;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe61b;</span>
+                <div class="name">会议</div>
+                <div class="code-name">&amp;#xe61b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe637;</span>
+                <div class="name">加班</div>
+                <div class="code-name">&amp;#xe637;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe665;</span>
+                <div class="name">表格</div>
+                <div class="code-name">&amp;#xe665;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xeb66;</span>
+                <div class="name">使用文档</div>
+                <div class="code-name">&amp;#xeb66;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe62e;</span>
+                <div class="name">多选框</div>
+                <div class="code-name">&amp;#xe62e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe751;</span>
+                <div class="name">单选</div>
+                <div class="code-name">&amp;#xe751;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe600;</span>
+                <div class="name">出租</div>
+                <div class="code-name">&amp;#xe600;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe647;</span>
+                <div class="name">招聘</div>
+                <div class="code-name">&amp;#xe647;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67d;</span>
+                <div class="name">财务</div>
+                <div class="code-name">&amp;#xe67d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe887;</span>
+                <div class="name">05采购</div>
+                <div class="code-name">&amp;#xe887;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68e;</span>
+                <div class="name">住房补贴</div>
+                <div class="code-name">&amp;#xe68e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe679;</span>
+                <div class="name">我的产品</div>
+                <div class="code-name">&amp;#xe679;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe63b;</span>
+                <div class="name">发票管理</div>
+                <div class="code-name">&amp;#xe63b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7e9;</span>
+                <div class="name">工资</div>
+                <div class="code-name">&amp;#xe7e9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe60c;</span>
+                <div class="name">住房补贴账户</div>
+                <div class="code-name">&amp;#xe60c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe613;</span>
+                <div class="name">维修</div>
+                <div class="code-name">&amp;#xe613;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe615;</span>
+                <div class="name">员工离职</div>
+                <div class="code-name">&amp;#xe615;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe616;</span>
+                <div class="name">招聘管理</div>
+                <div class="code-name">&amp;#xe616;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe603;</span>
+                <div class="name">财务</div>
+                <div class="code-name">&amp;#xe603;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe60d;</span>
+                <div class="name">请假申请</div>
+                <div class="code-name">&amp;#xe60d;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe722;</span>
+                <div class="name">出差</div>
+                <div class="code-name">&amp;#xe722;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67e;</span>
+                <div class="name">用餐就餐</div>
+                <div class="code-name">&amp;#xe67e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xea00;</span>
+                <div class="name">地图组织站点,层级,下级,组织架构布局</div>
+                <div class="code-name">&amp;#xea00;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe68a;</span>
+                <div class="name">合同</div>
+                <div class="code-name">&amp;#xe68a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6ca;</span>
+                <div class="name">补卡</div>
+                <div class="code-name">&amp;#xe6ca;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6c7;</span>
+                <div class="name">出差</div>
+                <div class="code-name">&amp;#xe6c7;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe726;</span>
+                <div class="name">报销申请-费用报销申请-02</div>
+                <div class="code-name">&amp;#xe726;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe676;</span>
+                <div class="name">11C分组,组织树</div>
+                <div class="code-name">&amp;#xe676;</div>
+              </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="unicode-">Unicode 引用</h2>
+          <hr>
+
+          <p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
+          <ul>
+            <li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
+            <li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
+          </ul>
+          <blockquote>
+            <p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
+          </blockquote>
+          <p>Unicode 使用步骤如下:</p>
+          <h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
+<pre><code class="language-css"
+>@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.woff2?t=1658393010708') format('woff2'),
+       url('iconfont.woff?t=1658393010708') format('woff'),
+       url('iconfont.ttf?t=1658393010708') format('truetype');
+}
+</code></pre>
+          <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
+<pre><code class="language-css"
+>.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
+<pre>
+<code class="language-html"
+>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
+</code></pre>
+          <blockquote>
+            <p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+          </blockquote>
+          </div>
+      </div>
+      <div class="content font-class">
+        <ul class="icon_lists dib-box">
+          
+          <li class="dib">
+            <span class="icon iconfont icon-iconfontkefu"></span>
+            <div class="name">
+              iconfont-kefu
+            </div>
+            <div class="code-name">.icon-iconfontkefu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-mima"></span>
+            <div class="name">
+              BBD密码
+            </div>
+            <div class="code-name">.icon-mima
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-renlishebao"></span>
+            <div class="name">
+              人力社保
+            </div>
+            <div class="code-name">.icon-renlishebao
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-bumen"></span>
+            <div class="name">
+              部门
+            </div>
+            <div class="code-name">.icon-bumen
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-charutupian"></span>
+            <div class="name">
+              插入图片
+            </div>
+            <div class="code-name">.icon-charutupian
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-kaoqinguanli"></span>
+            <div class="name">
+              考勤管理
+            </div>
+            <div class="code-name">.icon-kaoqinguanli
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-shenfenzheng"></span>
+            <div class="name">
+              身份证
+            </div>
+            <div class="code-name">.icon-shenfenzheng
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-weizhi"></span>
+            <div class="name">
+              位置
+            </div>
+            <div class="code-name">.icon-weizhi
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-24gf-phoneBubble"></span>
+            <div class="name">
+              24gf-phoneBubble
+            </div>
+            <div class="code-name">.icon-24gf-phoneBubble
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-kaoqin"></span>
+            <div class="name">
+              考勤
+            </div>
+            <div class="code-name">.icon-kaoqin
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-huiyi"></span>
+            <div class="name">
+              会议
+            </div>
+            <div class="code-name">.icon-huiyi
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-jiaban"></span>
+            <div class="name">
+              加班
+            </div>
+            <div class="code-name">.icon-jiaban
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-biaoge"></span>
+            <div class="name">
+              表格
+            </div>
+            <div class="code-name">.icon-biaoge
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-shiyongwendang"></span>
+            <div class="name">
+              使用文档
+            </div>
+            <div class="code-name">.icon-shiyongwendang
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-duoxuankuang"></span>
+            <div class="name">
+              多选框
+            </div>
+            <div class="code-name">.icon-duoxuankuang
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-danxuan"></span>
+            <div class="name">
+              单选
+            </div>
+            <div class="code-name">.icon-danxuan
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-chuzu"></span>
+            <div class="name">
+              出租
+            </div>
+            <div class="code-name">.icon-chuzu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-zhaopin"></span>
+            <div class="name">
+              招聘
+            </div>
+            <div class="code-name">.icon-zhaopin
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-caiwu"></span>
+            <div class="name">
+              财务
+            </div>
+            <div class="code-name">.icon-caiwu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-caigou"></span>
+            <div class="name">
+              05采购
+            </div>
+            <div class="code-name">.icon-caigou
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-zhufangbutie"></span>
+            <div class="name">
+              住房补贴
+            </div>
+            <div class="code-name">.icon-zhufangbutie
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-wodechanpin"></span>
+            <div class="name">
+              我的产品
+            </div>
+            <div class="code-name">.icon-wodechanpin
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-fapiaoguanli"></span>
+            <div class="name">
+              发票管理
+            </div>
+            <div class="code-name">.icon-fapiaoguanli
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-gongzi"></span>
+            <div class="name">
+              工资
+            </div>
+            <div class="code-name">.icon-gongzi
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-zhufangbutiezhanghu"></span>
+            <div class="name">
+              住房补贴账户
+            </div>
+            <div class="code-name">.icon-zhufangbutiezhanghu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-weixiu"></span>
+            <div class="name">
+              维修
+            </div>
+            <div class="code-name">.icon-weixiu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-yuangonglizhi"></span>
+            <div class="name">
+              员工离职
+            </div>
+            <div class="code-name">.icon-yuangonglizhi
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-zhaopinguanli"></span>
+            <div class="name">
+              招聘管理
+            </div>
+            <div class="code-name">.icon-zhaopinguanli
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-caiwu1"></span>
+            <div class="name">
+              财务
+            </div>
+            <div class="code-name">.icon-caiwu1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-qingjiashenqing"></span>
+            <div class="name">
+              请假申请
+            </div>
+            <div class="code-name">.icon-qingjiashenqing
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-ziyuan207"></span>
+            <div class="name">
+              出差
+            </div>
+            <div class="code-name">.icon-ziyuan207
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-yongcanjiucan"></span>
+            <div class="name">
+              用餐就餐
+            </div>
+            <div class="code-name">.icon-yongcanjiucan
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-map-site"></span>
+            <div class="name">
+              地图组织站点,层级,下级,组织架构布局
+            </div>
+            <div class="code-name">.icon-map-site
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-hetong"></span>
+            <div class="name">
+              合同
+            </div>
+            <div class="code-name">.icon-hetong
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-buka"></span>
+            <div class="name">
+              补卡
+            </div>
+            <div class="code-name">.icon-buka
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-chucha"></span>
+            <div class="name">
+              出差
+            </div>
+            <div class="code-name">.icon-chucha
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-baoxiaoshenqing-feiyongbaoxiaoshenqing-02"></span>
+            <div class="name">
+              报销申请-费用报销申请-02
+            </div>
+            <div class="code-name">.icon-baoxiaoshenqing-feiyongbaoxiaoshenqing-02
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-11Cfenzuzuzhishu"></span>
+            <div class="name">
+              11C分组,组织树
+            </div>
+            <div class="code-name">.icon-a-11Cfenzuzuzhishu
+            </div>
+          </li>
+          
+        </ul>
+        <div class="article markdown">
+        <h2 id="font-class-">font-class 引用</h2>
+        <hr>
+
+        <p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
+        <p>与 Unicode 使用方式相比,具有如下特点:</p>
+        <ul>
+          <li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
+          <li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
+        </ul>
+        <p>使用步骤如下:</p>
+        <h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
+<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
+</code></pre>
+        <h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
+</code></pre>
+        <blockquote>
+          <p>"
+            iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
+        </blockquote>
+      </div>
+      </div>
+      <div class="content symbol">
+          <ul class="icon_lists dib-box">
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-iconfontkefu"></use>
+                </svg>
+                <div class="name">iconfont-kefu</div>
+                <div class="code-name">#icon-iconfontkefu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-mima"></use>
+                </svg>
+                <div class="name">BBD密码</div>
+                <div class="code-name">#icon-mima</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-renlishebao"></use>
+                </svg>
+                <div class="name">人力社保</div>
+                <div class="code-name">#icon-renlishebao</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-bumen"></use>
+                </svg>
+                <div class="name">部门</div>
+                <div class="code-name">#icon-bumen</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-charutupian"></use>
+                </svg>
+                <div class="name">插入图片</div>
+                <div class="code-name">#icon-charutupian</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-kaoqinguanli"></use>
+                </svg>
+                <div class="name">考勤管理</div>
+                <div class="code-name">#icon-kaoqinguanli</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-shenfenzheng"></use>
+                </svg>
+                <div class="name">身份证</div>
+                <div class="code-name">#icon-shenfenzheng</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-weizhi"></use>
+                </svg>
+                <div class="name">位置</div>
+                <div class="code-name">#icon-weizhi</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-24gf-phoneBubble"></use>
+                </svg>
+                <div class="name">24gf-phoneBubble</div>
+                <div class="code-name">#icon-24gf-phoneBubble</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-kaoqin"></use>
+                </svg>
+                <div class="name">考勤</div>
+                <div class="code-name">#icon-kaoqin</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-huiyi"></use>
+                </svg>
+                <div class="name">会议</div>
+                <div class="code-name">#icon-huiyi</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-jiaban"></use>
+                </svg>
+                <div class="name">加班</div>
+                <div class="code-name">#icon-jiaban</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-biaoge"></use>
+                </svg>
+                <div class="name">表格</div>
+                <div class="code-name">#icon-biaoge</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-shiyongwendang"></use>
+                </svg>
+                <div class="name">使用文档</div>
+                <div class="code-name">#icon-shiyongwendang</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-duoxuankuang"></use>
+                </svg>
+                <div class="name">多选框</div>
+                <div class="code-name">#icon-duoxuankuang</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-danxuan"></use>
+                </svg>
+                <div class="name">单选</div>
+                <div class="code-name">#icon-danxuan</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-chuzu"></use>
+                </svg>
+                <div class="name">出租</div>
+                <div class="code-name">#icon-chuzu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-zhaopin"></use>
+                </svg>
+                <div class="name">招聘</div>
+                <div class="code-name">#icon-zhaopin</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-caiwu"></use>
+                </svg>
+                <div class="name">财务</div>
+                <div class="code-name">#icon-caiwu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-caigou"></use>
+                </svg>
+                <div class="name">05采购</div>
+                <div class="code-name">#icon-caigou</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-zhufangbutie"></use>
+                </svg>
+                <div class="name">住房补贴</div>
+                <div class="code-name">#icon-zhufangbutie</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-wodechanpin"></use>
+                </svg>
+                <div class="name">我的产品</div>
+                <div class="code-name">#icon-wodechanpin</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-fapiaoguanli"></use>
+                </svg>
+                <div class="name">发票管理</div>
+                <div class="code-name">#icon-fapiaoguanli</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-gongzi"></use>
+                </svg>
+                <div class="name">工资</div>
+                <div class="code-name">#icon-gongzi</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-zhufangbutiezhanghu"></use>
+                </svg>
+                <div class="name">住房补贴账户</div>
+                <div class="code-name">#icon-zhufangbutiezhanghu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-weixiu"></use>
+                </svg>
+                <div class="name">维修</div>
+                <div class="code-name">#icon-weixiu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-yuangonglizhi"></use>
+                </svg>
+                <div class="name">员工离职</div>
+                <div class="code-name">#icon-yuangonglizhi</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-zhaopinguanli"></use>
+                </svg>
+                <div class="name">招聘管理</div>
+                <div class="code-name">#icon-zhaopinguanli</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-caiwu1"></use>
+                </svg>
+                <div class="name">财务</div>
+                <div class="code-name">#icon-caiwu1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-qingjiashenqing"></use>
+                </svg>
+                <div class="name">请假申请</div>
+                <div class="code-name">#icon-qingjiashenqing</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-ziyuan207"></use>
+                </svg>
+                <div class="name">出差</div>
+                <div class="code-name">#icon-ziyuan207</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-yongcanjiucan"></use>
+                </svg>
+                <div class="name">用餐就餐</div>
+                <div class="code-name">#icon-yongcanjiucan</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-map-site"></use>
+                </svg>
+                <div class="name">地图组织站点,层级,下级,组织架构布局</div>
+                <div class="code-name">#icon-map-site</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-hetong"></use>
+                </svg>
+                <div class="name">合同</div>
+                <div class="code-name">#icon-hetong</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-buka"></use>
+                </svg>
+                <div class="name">补卡</div>
+                <div class="code-name">#icon-buka</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-chucha"></use>
+                </svg>
+                <div class="name">出差</div>
+                <div class="code-name">#icon-chucha</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-baoxiaoshenqing-feiyongbaoxiaoshenqing-02"></use>
+                </svg>
+                <div class="name">报销申请-费用报销申请-02</div>
+                <div class="code-name">#icon-baoxiaoshenqing-feiyongbaoxiaoshenqing-02</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-11Cfenzuzuzhishu"></use>
+                </svg>
+                <div class="name">11C分组,组织树</div>
+                <div class="code-name">#icon-a-11Cfenzuzuzhishu</div>
+            </li>
+          
+          </ul>
+          <div class="article markdown">
+          <h2 id="symbol-">Symbol 引用</h2>
+          <hr>
+
+          <p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
+            这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
+          <ul>
+            <li>支持多色图标了,不再受单色限制。</li>
+            <li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
+            <li>兼容性较差,支持 IE9+,及现代浏览器。</li>
+            <li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
+          </ul>
+          <p>使用步骤如下:</p>
+          <h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
+<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
+</code></pre>
+          <h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
+<pre><code class="language-html">&lt;style&gt;
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+&lt;/style&gt;
+</code></pre>
+          <h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
+<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
+  &lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
+&lt;/svg&gt;
+</code></pre>
+          </div>
+      </div>
+
+    </div>
+  </div>
+  <script>
+  $(document).ready(function () {
+      $('.tab-container .content:first').show()
+
+      $('#tabs li').click(function (e) {
+        var tabContent = $('.tab-container .content')
+        var index = $(this).index()
+
+        if ($(this).hasClass('active')) {
+          return
+        } else {
+          $('#tabs li').removeClass('active')
+          $(this).addClass('active')
+
+          tabContent.hide().eq(index).fadeIn()
+        }
+      })
+    })
+  </script>
+</body>
+</html>

+ 168 - 0
src/assets/iconfont/iconfont.css

@@ -0,0 +1,168 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 3538338 */
+  src: url('iconfont.woff2?t=1658393010708') format('woff2'),
+       url('iconfont.woff?t=1658393010708') format('woff'),
+       url('iconfont.ttf?t=1658393010708') format('truetype');
+}
+
+/*.iconfont {*/
+[class^="iconfont"],[class*="iconfont"]{
+  font-family: "iconfont" !important;
+  font-size: 12px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-iconfontkefu:before {
+  content: "\e61c";
+}
+
+.icon-mima:before {
+  content: "\e648";
+}
+
+.icon-renlishebao:before {
+  content: "\e636";
+}
+
+.icon-bumen:before {
+  content: "\e758";
+}
+
+.icon-charutupian:before {
+  content: "\ec7f";
+}
+
+.icon-kaoqinguanli:before {
+  content: "\e610";
+}
+
+.icon-shenfenzheng:before {
+  content: "\e614";
+}
+
+.icon-weizhi:before {
+  content: "\e64b";
+}
+
+.icon-24gf-phoneBubble:before {
+  content: "\e966";
+}
+
+.icon-kaoqin:before {
+  content: "\e643";
+}
+
+.icon-huiyi:before {
+  content: "\e61b";
+}
+
+.icon-jiaban:before {
+  content: "\e637";
+}
+
+.icon-biaoge:before {
+  content: "\e665";
+}
+
+.icon-shiyongwendang:before {
+  content: "\eb66";
+}
+
+.icon-duoxuankuang:before {
+  content: "\e62e";
+}
+
+.icon-danxuan:before {
+  content: "\e751";
+}
+
+.icon-chuzu:before {
+  content: "\e600";
+}
+
+.icon-zhaopin:before {
+  content: "\e647";
+}
+
+.icon-caiwu:before {
+  content: "\e67d";
+}
+
+.icon-caigou:before {
+  content: "\e887";
+}
+
+.icon-zhufangbutie:before {
+  content: "\e68e";
+}
+
+.icon-wodechanpin:before {
+  content: "\e679";
+}
+
+.icon-fapiaoguanli:before {
+  content: "\e63b";
+}
+
+.icon-gongzi:before {
+  content: "\e7e9";
+}
+
+.icon-zhufangbutiezhanghu:before {
+  content: "\e60c";
+}
+
+.icon-weixiu:before {
+  content: "\e613";
+}
+
+.icon-yuangonglizhi:before {
+  content: "\e615";
+}
+
+.icon-zhaopinguanli:before {
+  content: "\e616";
+}
+
+.icon-caiwu1:before {
+  content: "\e603";
+}
+
+.icon-qingjiashenqing:before {
+  content: "\e60d";
+}
+
+.icon-ziyuan207:before {
+  content: "\e722";
+}
+
+.icon-yongcanjiucan:before {
+  content: "\e67e";
+}
+
+.icon-map-site:before {
+  content: "\ea00";
+}
+
+.icon-hetong:before {
+  content: "\e68a";
+}
+
+.icon-buka:before {
+  content: "\e6ca";
+}
+
+.icon-chucha:before {
+  content: "\e6c7";
+}
+
+.icon-baoxiaoshenqing-feiyongbaoxiaoshenqing-02:before {
+  content: "\e726";
+}
+
+.icon-a-11Cfenzuzuzhishu:before {
+  content: "\e676";
+}
+

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/iconfont/iconfont.js


+ 268 - 0
src/assets/iconfont/iconfont.json

@@ -0,0 +1,268 @@
+{
+  "id": "3538338",
+  "name": "wflow",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "807897",
+      "name": "iconfont-kefu",
+      "font_class": "iconfontkefu",
+      "unicode": "e61c",
+      "unicode_decimal": 58908
+    },
+    {
+      "icon_id": "1313126",
+      "name": "BBD密码",
+      "font_class": "mima",
+      "unicode": "e648",
+      "unicode_decimal": 58952
+    },
+    {
+      "icon_id": "2131309",
+      "name": "人力社保",
+      "font_class": "renlishebao",
+      "unicode": "e636",
+      "unicode_decimal": 58934
+    },
+    {
+      "icon_id": "4774868",
+      "name": "部门",
+      "font_class": "bumen",
+      "unicode": "e758",
+      "unicode_decimal": 59224
+    },
+    {
+      "icon_id": "6337457",
+      "name": "插入图片",
+      "font_class": "charutupian",
+      "unicode": "ec7f",
+      "unicode_decimal": 60543
+    },
+    {
+      "icon_id": "2958951",
+      "name": "考勤管理",
+      "font_class": "kaoqinguanli",
+      "unicode": "e610",
+      "unicode_decimal": 58896
+    },
+    {
+      "icon_id": "3007689",
+      "name": "身份证",
+      "font_class": "shenfenzheng",
+      "unicode": "e614",
+      "unicode_decimal": 58900
+    },
+    {
+      "icon_id": "5121522",
+      "name": "位置",
+      "font_class": "weizhi",
+      "unicode": "e64b",
+      "unicode_decimal": 58955
+    },
+    {
+      "icon_id": "7568869",
+      "name": "24gf-phoneBubble",
+      "font_class": "24gf-phoneBubble",
+      "unicode": "e966",
+      "unicode_decimal": 59750
+    },
+    {
+      "icon_id": "11134714",
+      "name": "考勤",
+      "font_class": "kaoqin",
+      "unicode": "e643",
+      "unicode_decimal": 58947
+    },
+    {
+      "icon_id": "15972093",
+      "name": "会议",
+      "font_class": "huiyi",
+      "unicode": "e61b",
+      "unicode_decimal": 58907
+    },
+    {
+      "icon_id": "19883444",
+      "name": "加班",
+      "font_class": "jiaban",
+      "unicode": "e637",
+      "unicode_decimal": 58935
+    },
+    {
+      "icon_id": "1392555",
+      "name": "表格",
+      "font_class": "biaoge",
+      "unicode": "e665",
+      "unicode_decimal": 58981
+    },
+    {
+      "icon_id": "3868276",
+      "name": "使用文档",
+      "font_class": "shiyongwendang",
+      "unicode": "eb66",
+      "unicode_decimal": 60262
+    },
+    {
+      "icon_id": "5881147",
+      "name": "多选框",
+      "font_class": "duoxuankuang",
+      "unicode": "e62e",
+      "unicode_decimal": 58926
+    },
+    {
+      "icon_id": "26323690",
+      "name": "单选",
+      "font_class": "danxuan",
+      "unicode": "e751",
+      "unicode_decimal": 59217
+    },
+    {
+      "icon_id": "5032",
+      "name": "出租",
+      "font_class": "chuzu",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "1079372",
+      "name": "招聘",
+      "font_class": "zhaopin",
+      "unicode": "e647",
+      "unicode_decimal": 58951
+    },
+    {
+      "icon_id": "1183143",
+      "name": "财务",
+      "font_class": "caiwu",
+      "unicode": "e67d",
+      "unicode_decimal": 59005
+    },
+    {
+      "icon_id": "1727267",
+      "name": "05采购",
+      "font_class": "caigou",
+      "unicode": "e887",
+      "unicode_decimal": 59527
+    },
+    {
+      "icon_id": "1876349",
+      "name": "我的产品",
+      "font_class": "wodechanpin",
+      "unicode": "e679",
+      "unicode_decimal": 59001
+    },
+    {
+      "icon_id": "1977843",
+      "name": "发票管理",
+      "font_class": "fapiaoguanli",
+      "unicode": "e63b",
+      "unicode_decimal": 58939
+    },
+    {
+      "icon_id": "7790995",
+      "name": "工资",
+      "font_class": "gongzi",
+      "unicode": "e7e9",
+      "unicode_decimal": 59369
+    },
+    {
+      "icon_id": "10120009",
+      "name": "住房补贴账户",
+      "font_class": "zhufangbutiezhanghu",
+      "unicode": "e60c",
+      "unicode_decimal": 58892
+    },
+    {
+      "icon_id": "11435446",
+      "name": "维修",
+      "font_class": "weixiu",
+      "unicode": "e613",
+      "unicode_decimal": 58899
+    },
+    {
+      "icon_id": "11435453",
+      "name": "员工离职",
+      "font_class": "yuangonglizhi",
+      "unicode": "e615",
+      "unicode_decimal": 58901
+    },
+    {
+      "icon_id": "11435456",
+      "name": "招聘管理",
+      "font_class": "zhaopinguanli",
+      "unicode": "e616",
+      "unicode_decimal": 58902
+    },
+    {
+      "icon_id": "12911861",
+      "name": "财务",
+      "font_class": "caiwu1",
+      "unicode": "e603",
+      "unicode_decimal": 58883
+    },
+    {
+      "icon_id": "14443545",
+      "name": "请假申请",
+      "font_class": "qingjiashenqing",
+      "unicode": "e60d",
+      "unicode_decimal": 58893
+    },
+    {
+      "icon_id": "14947326",
+      "name": "出差",
+      "font_class": "ziyuan207",
+      "unicode": "e722",
+      "unicode_decimal": 59170
+    },
+    {
+      "icon_id": "17187052",
+      "name": "用餐就餐",
+      "font_class": "yongcanjiucan",
+      "unicode": "e67e",
+      "unicode_decimal": 59006
+    },
+    {
+      "icon_id": "18170995",
+      "name": "地图组织站点,层级,下级,组织架构布局",
+      "font_class": "map-site",
+      "unicode": "ea00",
+      "unicode_decimal": 59904
+    },
+    {
+      "icon_id": "21053836",
+      "name": "合同",
+      "font_class": "hetong",
+      "unicode": "e68a",
+      "unicode_decimal": 59018
+    },
+    {
+      "icon_id": "21159370",
+      "name": "补卡",
+      "font_class": "buka",
+      "unicode": "e6ca",
+      "unicode_decimal": 59082
+    },
+    {
+      "icon_id": "24080655",
+      "name": "出差",
+      "font_class": "chucha",
+      "unicode": "e6c7",
+      "unicode_decimal": 59079
+    },
+    {
+      "icon_id": "24283254",
+      "name": "报销申请-费用报销申请-02",
+      "font_class": "baoxiaoshenqing-feiyongbaoxiaoshenqing-02",
+      "unicode": "e726",
+      "unicode_decimal": 59174
+    },
+    {
+      "icon_id": "29522596",
+      "name": "11C分组,组织树",
+      "font_class": "a-11Cfenzuzuzhishu",
+      "unicode": "e676",
+      "unicode_decimal": 58998
+    }
+  ]
+}

BIN
src/assets/iconfont/iconfont.ttf


BIN
src/assets/iconfont/iconfont.woff


BIN
src/assets/iconfont/iconfont.woff2


BIN
src/assets/images/add-close.png


BIN
src/assets/images/add-close1.png


BIN
src/assets/images/cancel.png


BIN
src/assets/images/check_box.png


BIN
src/assets/images/close-circle.png


BIN
src/assets/images/delete.png


BIN
src/assets/images/left-arrow.png


BIN
src/assets/images/minus-90-90.png


BIN
src/assets/images/plus-90-90.png


BIN
src/assets/images/right-arrow.png


BIN
src/assets/images/warning.png


+ 14 - 0
src/assets/theme.less

@@ -0,0 +1,14 @@
+//主题定制
+
+@theme-primary: #1890FF; //主题色,应当与element-ui一致
+@theme-danger: #f56c6c; //主题色,应当与element-ui一致
+
+//审批流程节点配色
+@node-root: #576a95; //发起人
+@node-condition: #15bca3; //条件
+@node-cc: #3296fa; //抄送
+@node-concurrent: #718dff; //并行
+@node-approval: #ff943e;  //审批
+@node-delay: #f25643; //延时
+@node-trigger: #47bc82; //触发器
+

+ 54 - 0
src/components/common/Ellipsis.vue

@@ -0,0 +1,54 @@
+<template>
+  <div :class="{'line': row === 1, 'lines': row > 1}"
+       :title="hoverTip ? content: null"
+       :style="{'--row':row}">
+    <slot name="pre"></slot>
+    {{content}}
+  </div>
+</template>
+
+<script>
+//超出指定行数自动隐藏文字
+export default {
+  name: "Ellipsis",
+  install(Vue){
+    Vue.component('ellipsis', this)
+  },
+  components: {},
+  props:{
+    row: {
+      type: Number,
+      default: 1
+    },
+    hoverTip:{
+      type: Boolean,
+      default: false
+    },
+    content:{
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+
+.line{
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.lines{
+  display: -webkit-box;
+  word-break: break-all;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: var(--row);
+  -webkit-box-orient: vertical;
+}
+</style>

+ 464 - 0
src/components/common/OrgPicker.vue

@@ -0,0 +1,464 @@
+<template>
+  <w-dialog :border="false" closeFree width="600px" @ok="selectOk" :title="title" v-model="visible">
+    <div class="picker">
+      <div class="candidate" v-loading="loading">
+        <div v-if="type !== 'role'">
+          <el-input v-model="search" @input="searchUser" style="width: 95%;" size="small"
+                    clearable placeholder="搜索人员,支持拼音、姓名" prefix-icon="el-icon-search"/>
+          <div v-show="!showUsers">
+            <ellipsis hoverTip style="height: 18px; color: #8c8c8c; padding: 5px 0 0" :row="1" :content="deptStackStr">
+              <i slot="pre" class="el-icon-office-building"></i>
+            </ellipsis>
+            <div style="margin-top: 5px">
+              <el-checkbox v-model="checkAll" @change="handleCheckAllChange" :disabled="!multiple">全选</el-checkbox>
+              <span v-show="deptStack.length > 0" class="top-dept" @click="beforeNode">上一级</span>
+            </div>
+          </div>
+        </div>
+        <div class="role-header" v-else>
+          <div>系统角色</div>
+        </div>
+        <div class="org-items" :style="type === 'role' ? 'height: 350px':''">
+          <el-empty :image-size="100" description="似乎没有数据" v-show="orgs.length === 0"/>
+          <div v-for="(org, index) in orgs" :key="index" :class="orgItemClass(org)" @click="selectChange(org)">
+            <el-checkbox :value="org.selected" @change="selectChange(org)" :disabled="disableDept(org)"></el-checkbox>
+            <div v-if="org.type === 'dept'">
+              <i class="el-icon-folder-opened"></i>
+              <span class="name">{{ org.name }}</span>
+              <span @click.stop="nextNode(org)" :class="`next-dept${org.selected ? '-disable':''}`">
+                <i class="iconfont icon-map-site"></i>下级
+              </span>
+            </div>
+            <div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
+              <el-avatar :size="35" :src="org.avatar" v-if="$isNotEmpty(org.avatar)"/>
+              <span v-else class="avatar">{{getShortName(org.name)}}</span>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <div style="display: inline-block" v-else>
+              <i class="iconfont icon-bumen"></i>
+              <span class="name">{{ org.name }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="selected">
+        <div class="count">
+          <span>已选 {{ select.length }} 项</span>
+          <span @click="clearSelected">清空</span>
+        </div>
+        <div class="org-items" style="height: 350px;">
+          <el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="select.length === 0"/>
+          <div v-for="(org, index) in select" :key="index" :class="orgItemClass(org)" >
+            <div v-if="org.type === 'dept'">
+              <i class="el-icon-folder-opened"></i>
+              <span style="position: static" class="name">{{ org.name }}</span>
+            </div>
+            <div v-else-if="org.type === 'user'" style="display: flex; align-items: center">
+              <el-avatar :size="35" :src="org.avatar" v-if="$isNotEmpty(org.avatar)"/>
+              <span v-else class="avatar">{{getShortName(org.name)}}</span>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <div v-else>
+              <i class="iconfont icon-bumen"></i>
+              <span class="name">{{ org.name }}</span>
+            </div>
+            <i class="el-icon-close" @click="noSelected(index)"></i>
+          </div>
+        </div>
+      </div>
+    </div>
+  </w-dialog>
+</template>
+
+<script>
+import {getOrgTree, getUserByName} from '@/utils/api/org'
+
+export default {
+  name: "OrgPicker",
+  components: {},
+  props: {
+    title: {
+      default: '请选择',
+      type: String
+    },
+    type: {
+      default: 'org', //org选择部门/人员  user-选人  dept-选部门 role-选角色
+      type: String
+    },
+    multiple: { //是否多选
+      default: false,
+      type: Boolean
+    },
+    selected: {
+      default: () => {
+        return []
+      },
+      type: Array
+    },
+  },
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      checkAll: false,
+      nowDeptId: null,
+      isIndeterminate: false,
+      searchUsers: [],
+      nodes: [],
+      select: [],
+      search: '',
+      deptStack: []
+    }
+  },
+  computed: {
+    deptStackStr() {
+      return String(this.deptStack.map(v => v.name)).replaceAll(',', ' > ')
+    },
+    orgs() {
+      return !this.search || this.search.trim() === '' ? this.nodes : this.searchUsers
+    },
+    showUsers(){
+      return this.search || this.search.trim() !== ''
+    }
+  },
+  methods: {
+    show() {
+      this.visible = true
+      this.init()
+      this.getOrgList()
+    },
+    orgItemClass(org){
+      return {
+        'org-item': true,
+        'org-dept-item': org.type === 'dept',
+        'org-user-item': org.type === 'user',
+        'org-role-item': org.type === 'role'
+      }
+    },
+    disableDept(node) {
+      return this.type === 'user' && 'dept' === node.type
+    },
+    getOrgList() {
+      this.loading = true
+      getOrgTree({deptId: this.nowDeptId, type: this.type}).then(rsp => {
+        this.loading = false
+        this.nodes = rsp.data
+        this.selectToLeft()
+      }).catch(err => {
+        this.loading = false
+        this.$message.error(err.response.data)
+      })
+    },
+    getShortName(name) {
+      if (name) {
+        return name.length > 2 ? name.substring(1, 3) : name;
+      }
+      return '**'
+    },
+    searchUser() {
+      let userName = this.search.trim()
+      this.searchUsers = []
+      this.loading = true
+      getUserByName({userName: userName}).then(rsp => {
+        this.loading = false
+        this.searchUsers = rsp.data
+        this.selectToLeft()
+      }).catch(err => {
+        this.loading = false
+        this.$message.error("接口异常")
+      })
+    },
+    selectToLeft() {
+      let nodes = this.search.trim() === '' ? this.nodes : this.searchUsers;
+      nodes.forEach(node => {
+        for (let i = 0; i < this.select.length; i++) {
+          if (this.select[i].id === node.id) {
+            node.selected = true;
+            break;
+          } else {
+            node.selected = false;
+          }
+        }
+      })
+    },
+    selectChange(node) {
+      if (node.selected) {
+        this.checkAll = false;
+        for (let i = 0; i < this.select.length; i++) {
+          if (this.select[i].id === node.id) {
+            this.select.splice(i, 1);
+            break;
+          }
+        }
+        node.selected = false;
+      } else if (!this.disableDept(node)) {
+        node.selected = true
+        let nodes = this.search.trim() === '' ? this.nodes : this.searchUsers;
+        if (!this.multiple) {
+          nodes.forEach(nd => {
+            if (node.id !== nd.id) {
+              nd.selected = false
+            }
+          })
+        }
+        if (node.type === 'dept') {
+          if (!this.multiple) {
+            this.select = [node]
+          } else {
+            this.select.unshift(node);
+          }
+        } else {
+          if (!this.multiple) {
+            this.select = [node]
+          } else {
+            this.select.push(node);
+          }
+        }
+      }
+    },
+    noSelected(index) {
+      let nodes = this.nodes;
+      for (let f = 0; f < 2; f++) {
+        for (let i = 0; i < nodes.length; i++) {
+          if (nodes[i].id === this.select[index].id) {
+            nodes[i].selected = false;
+            this.checkAll = false;
+            break;
+          }
+        }
+        nodes = this.searchUsers;
+      }
+      this.select.splice(index, 1)
+    },
+    handleCheckAllChange() {
+      this.nodes.forEach(node => {
+        if (this.checkAll) {
+          if (!node.selected && !this.disableDept(node)) {
+            node.selected = true
+            this.select.push(node)
+          }
+        } else {
+          node.selected = false;
+          for (let i = 0; i < this.select.length; i++) {
+            if (this.select[i].id === node.id) {
+              this.select.splice(i, 1);
+              break;
+            }
+          }
+        }
+      })
+    },
+    nextNode(node) {
+      this.nowDeptId = node.id
+      this.deptStack.push(node)
+      this.getOrgList()
+    },
+    beforeNode() {
+      if (this.deptStack.length === 0) {
+        return;
+      }
+      if (this.deptStack.length < 2) {
+        this.nowDeptId = null
+      } else {
+        this.nowDeptId = this.deptStack[this.deptStack.length - 2].id
+      }
+      this.deptStack.splice(this.deptStack.length - 1, 1);
+      this.getOrgList()
+    },
+    recover() {
+      this.select = []
+      this.nodes.forEach(nd => nd.selected = false)
+    },
+    selectOk() {
+      this.$emit('ok', Object.assign([], this.select.map(v => {
+        v.avatar = undefined
+        return v
+      })))
+      this.visible = false
+      this.recover()
+    },
+    clearSelected(){
+      this.$confirm('您确定要清空已选中的项?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.recover()
+      })
+    },
+    close() {
+      this.$emit('close')
+      this.recover()
+    },
+    init() {
+      this.checkAll = false;
+      this.nowDeptId = null;
+      this.deptStack = [];
+      this.nodes = []
+      this.select = Object.assign([], this.selected)
+      this.selectToLeft()
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+
+@containWidth: 278px;
+
+.candidate, .selected {
+  position: absolute;
+  display: inline-block;
+  width: @containWidth;
+  height: 400px;
+  border: 1px solid #e8e8e8;
+}
+
+.picker {
+  height: 402px;
+  position: relative;
+  text-align: left;
+  .candidate {
+    left: 0;
+    top: 0;
+
+    .role-header{
+      padding: 10px !important;
+      margin-bottom: 5px;
+      border-bottom: 1px solid #e8e8e8;
+    }
+
+    .top-dept{
+      margin-left: 20px;
+      cursor: pointer;
+      color:#38adff;
+    }
+    .next-dept {
+      float: right;
+      color: #1890FF;
+      cursor: pointer;
+    }
+
+    .next-dept-disable {
+      float: right;
+      color: #8c8c8c;
+      cursor: not-allowed;
+    }
+
+    & > div:first-child {
+      padding: 5px 10px;
+    }
+
+  }
+
+  .selected {
+    right: 0;
+    top: 0;
+  }
+
+  .org-items {
+    overflow-y: auto;
+    height: 310px;
+
+    .el-icon-close {
+      position: absolute;
+      right: 5px;
+      cursor: pointer;
+      font-size: larger;
+    }
+    .org-dept-item {
+      padding: 10px 5px;
+
+      & > div {
+        display: inline-block;
+
+        & > span:last-child {
+          position: absolute;
+          right: 5px;
+        }
+      }
+    }
+
+    .org-role-item {
+      display: flex;
+      align-items: center;
+      padding: 10px 5px;
+    }
+
+    /deep/ .org-user-item {
+      display: flex;
+      align-items: center;
+      padding: 5px;
+
+      & > div {
+        display: inline-block;
+      }
+
+      .avatar {
+        width: 35px;
+        text-align: center;
+        line-height: 35px;
+        background: #1890FF;
+        color: white;
+        border-radius: 50%;
+      }
+    }
+
+    /deep/ .org-item {
+      margin: 0 5px;
+      border-radius: 5px;
+      position: relative;
+
+      .el-checkbox {
+        margin-right: 10px;
+      }
+
+      .name {
+        margin-left: 5px;
+      }
+
+      &:hover {
+        background: #f1f1f1;
+      }
+    }
+  }
+}
+
+.selected {
+  border-left: none;
+
+  .count {
+    width: calc(@containWidth - 20px);
+    padding: 10px;
+    display: inline-block;
+    border-bottom: 1px solid #e8e8e8;
+    margin-bottom: 5px;
+    & > span:nth-child(2) {
+      float: right;
+      color: #c75450;
+      cursor: pointer;
+    }
+  }
+}
+
+/deep/ .el-dialog__body {
+  padding: 10px 20px;
+}
+
+.disabled{
+  cursor: not-allowed !important;
+  color: #8c8c8c !important;
+}
+
+::-webkit-scrollbar {
+  float: right;
+  width: 4px;
+  height: 4px;
+  background-color: white;
+}
+
+::-webkit-scrollbar-thumb {
+  border-radius: 16px;
+  background-color: #efefef;
+}
+</style>

+ 36 - 0
src/components/common/Tip.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-tooltip :effect="isDark ? 'dark':'light'" :content="content" placement="top-start">
+    <div>
+      <slot></slot>
+      <i class="el-icon-question" style="margin: 0 0px"></i>
+    </div>
+  </el-tooltip>
+</template>
+
+<script>
+export default {
+  install(Vue) {
+    Vue.component('Tip', this)
+  },
+  name: "Tip",
+  components: {},
+  props:{
+    isDark:{
+      type: Boolean,
+      default: false
+    },
+    content:{
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 103 - 0
src/components/common/WDialog.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-dialog custom-class="custom-dialog" class="border" :width="width" :title="title"
+             append-to-body :close-on-click-modal="clickClose"
+             :destroy-on-close="closeFree" :visible.sync="_value">
+    <slot></slot>
+    <div slot="footer" v-if="showFooter">
+      <el-button size="mini" @click="_value = false; $emit('cancel')">{{ cancelText }}</el-button>
+      <el-button size="mini" type="primary" @click="$emit('ok')">{{ okText }}</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: "WDialog",
+  install(Vue) {
+    Vue.component('WDialog', this)
+  },
+  components: {},
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: '50%'
+    },
+    value: {
+      type: Boolean,
+      default: false
+    },
+    clickClose: {
+      type: Boolean,
+      default: false
+    },
+    closeFree: {
+      type: Boolean,
+      default: false
+    },
+    showFooter: {
+      type: Boolean,
+      default: true
+    },
+    cancelText: {
+      type: String,
+      default: '取 消'
+    },
+    okText: {
+      type: String,
+      default: '确 定'
+    },
+    border:{
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    _value: {
+      get() {
+        return this.value;
+      },
+      set(val) {
+        this.$emit("input", val);
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {}
+}
+</script>
+
+<style lang="less" scoped>
+/deep/ .custom-dialog {
+  .el-dialog__header {
+    padding: 10px 20px;
+    .el-dialog__title{
+      font-size: 17px;
+    }
+    .el-dialog__headerbtn{
+      top: 15px;
+      .i{
+        font-size: large;
+      }
+    }
+  }
+
+  .el-dialog__footer {
+    padding: 10px 20px;
+  }
+}
+
+ .border {
+   /deep/ .el-dialog__header {
+    border-bottom: 1px solid #e8e8e8;
+  }
+   /deep/ .el-dialog__footer {
+    border-top: 1px solid #e8e8e8;
+  }
+}
+</style>

+ 189 - 0
src/components/organization/BasicSetting.vue

@@ -0,0 +1,189 @@
+<template>
+    <div class="my-nav-content">
+        <div class="form-container">
+            <el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="auto"
+                style="max-width: 600px;margin: auto;">
+                <el-form-item label="流程编号111" prop="formCode">
+                    <el-input v-model="form.formCode" disabled placeholder="请输入流程编号" :style="{ width: '100%' }" />
+                </el-form-item>
+                <el-form-item label="选择分组" prop="flowGroup">
+                    <el-select v-model="form.flowGroup" placeholder="请选择选择分组" :style="{ width: '100%' }">
+                        <el-option v-for="(item, index) in flowOptions" :key="index" :label="item.label"
+                            :value="item.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="审批名称" prop="bpmnName">
+                    <el-input v-model="form.bpmnName" placeholder="请输入审批名称" :style="{ width: '100%' }" />
+                </el-form-item>
+
+                <el-form-item label="审批人去重" prop="deduplicationType">
+                    <el-select v-model="form.deduplicationType" placeholder="请选择去重类型" :style="{ width: '100%' }">
+                        <el-option v-for="(item, index) in autoRepeatOptions" :key="index" :label="item.label"
+                            :value="item.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <!-- <el-form-item label="模板图标" prop="icon">
+                    <img :src="activeIconSrc" style="width: 28px;height: 28px;vertical-align: middle;">
+                    <el-button plain @click="dialogVisible = true" style="margin-left: 10px;">选择图标</el-button>
+                </el-form-item> -->
+                <el-form-item label="审批说明" prop="remark">
+                    <el-input v-model="form.remark" type="textarea" placeholder="请输入审批说明" :maxlength="100"
+                        show-word-limit :autosize="{ minRows: 4, maxRows: 4 }" :style="{ width: '100%' }"></el-input>
+                </el-form-item>
+            </el-form>
+        </div>
+        <!-- <el-dialog title="选择图标" v-model="dialogVisible" width="612px">
+            <img v-for="(icon, index) in iconList" :key="index" :src="icon.src" class="icon-item"
+                :class="{ active: selectedIcon === icon.id }" @click="selectedIcon = icon.id">
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button @click="dialogVisible = false; selectedIcon = activeIcon">关 闭</el-button>
+                    <el-button type="primary" @click="dialogVisible = false; activeIcon = selectedIcon">确 定</el-button>
+                </div>
+            </template>
+        </el-dialog> -->
+    </div>
+</template>
+
+<script>
+import { NodeUtils } from '@/utils/workflow/nodeUtils'
+export default {
+
+    
+    props: {
+        basicData: {
+            type: Object,
+            required: true,
+            default: () => (null)
+        }
+    },
+
+    data() {
+        return {
+            // fileImgs: import.meta.glob('/public/flowIcon/**/*'),
+            // fileImgKeys: Object.keys(this.fileImgs),
+            // iconList: this.fileImgKeys.map((t, idx) => ({ src: this.getPath(t), id: idx })) ,
+            form: {
+                bpmnName: '合同审批',
+                bpmnCode: this.generatorID,
+                bpmnType: null,
+                flowGroup: 1,
+                formCode: null,
+                remark: '',
+                effectiveStatus: true,
+                deduplicationType: 1
+            },
+            rules: {
+                formCode: [{
+                    required: true,
+                    message: '请输入流程编号',
+                    trigger: 'blur'
+                }],
+                bpmnName: [{
+                    required: true,
+                    message: '请输入审批名称',
+                    trigger: 'blur'
+                }],
+                flowGroup: [{
+                    required: true,
+                    message: '请选择选择分组',
+                    trigger: 'change'
+                }],
+            },
+            generatorID: "BIZ_" + NodeUtils.idGenerator(),
+            // iconList: NodeUtils.getIconList(),
+            dialogVisible: false,
+            // selectedIcon: this.iconList[0].id,
+            // activeIcon: this.iconList[0].id,
+            autoRepeatOptions: [{
+                "label": "不启用自动去重",
+                "value": 1
+            }, {
+                "label": "启用自动去重",
+                "value": 2
+                }],
+            flowOptions: [{
+                "label": "总公司流程",
+                "value": 1
+            }, {
+                "label": "分公司流程",
+                "value": 2
+            }]
+        }
+    },
+
+    computed: {
+        activeIconSrc() {
+            const selectedIconObj = this.iconList.find(icon => icon.id === this.activeIcon);
+            return selectedIconObj ? selectedIconObj.src : '';
+        }
+    },
+
+    async mounted() {
+        if (this.basicData) {
+            this.form.bpmnName = this.basicData.bpmnName;
+            this.form.bpmnCode = this.generatorID;
+            this.form.bpmnType = this.basicData.bpmnType;
+            this.form.formCode = this.basicData.formCode;
+            this.form.remark = this.basicData.remark;
+            this.form.effectiveStatus = this.basicData.effectiveStatus;
+            this.form.deduplicationType = this.basicData.deduplicationType;
+        }
+    },
+
+    methods: {
+        // getPath(metaUrl) {
+        //     let path = ''
+        //     if (import.meta.env.MODE !== 'development') {
+        //         const metaUrlMatch = metaUrl.match(/^(.*?)\/public\//) // 匹配public前面的路径
+        //         if (metaUrlMatch && metaUrlMatch[0]) {
+        //             path = metaUrl.replace(metaUrlMatch[0], '/ant-flow/dist/')
+        //         }
+        //     } else {
+        //         path = metaUrl
+        //     }
+        //     return path
+        // },
+        nextSubmit() {
+            if (!this.$refs.ruleFormRef) return
+            this.$refs.ruleFormRef.validate((valid, fields) => {
+                if (valid) {
+                    this.$emit('nextChange', { label: "流程设计", key: "processDesign" })
+                }
+            })
+        },
+        getData() {
+            return new Promise((resolve, reject) => {
+                this.$refs['ruleFormRef'].validate((valid, fields) => {
+                    if (!valid) {
+                        this.$emit('nextChange', { label: "基础设置", key: "basicSettingDesign" })
+                        return;
+                    }
+                    this.form.effectiveStatus = this.form.effectiveStatus ? 1 : 0;
+                    resolve({ formData: this.form })  // TODO 提交表单
+                })
+            })
+        }
+    }
+}
+
+</script>
+
+<style lang="scss" scoped>
+.icon-item {
+    width: 40px;
+    height: 40px;
+    margin: 6px;
+    position: relative;
+    cursor: pointer;
+    opacity: .5;
+
+    &.active {
+        opacity: 1;
+    }
+
+    &:hover {
+        opacity: 1;
+    }
+}
+</style>

+ 48 - 0
src/components/organization/CustomForm.vue

@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <v-form-designer ref="vfDesigner" :designer-config="myConfig"></v-form-designer>
+    <el-button type="primary" @click="saveFormDesign">保存设计</el-button>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FormDesignerDemo',
+  data() {
+    return {
+      myConfig: {
+        logoHeader: false, // 隐藏Logo
+        exportCodeButton: false, // 隐藏“导出代码”按钮
+        generateSFCButton: false, // 隐藏“生成SFC”按钮
+        formTemplates: false // 隐藏表单模板
+        // 更多配置见官方文档[citation:9]
+      }
+    };
+  },
+  methods: {
+    
+    saveFormDesign() {
+      // 1. 调用API获取当前设计的表单JSON对象[citation:9]
+      const formJson = this.$refs.vfDesigner.getFormJson();
+
+      // 2. 使用 $emit 将数据传递给父组件
+      this.$emit('save', formJson);
+
+      // 也可以提交到Vuex (假设有相应的mutation)
+      // this.$store.commit('form/UPDATE_DESIGN', formJson);
+
+      // 或者通过事件总线传递[citation:2][citation:4]
+      // this.$bus.$emit('form-designed', formJson);
+
+      console.log('表单JSON已获取:', formJson);
+    }
+  }
+}
+</script>
+
+<style scoped>
+body {
+  margin: 0;
+  /* 如果页面出现垂直滚动条,可以加入此样式消除 */
+}
+</style>

+ 167 - 0
src/components/organization/CustomProcess.vue

@@ -0,0 +1,167 @@
+<template>
+  <div>
+    <div class="my-nav">
+      <div class="my-nav-left">
+        <div class="my-nav-back">
+          <i class="flowicon flowicon-left"></i>
+        </div>
+        <div class="my-nav-title">{{ title }}</div>
+      </div>
+
+      <div class="my-nav-center">
+        <div class="step-tab">
+          <div v-for="(item, index) in steps" :key="index" class="step"
+            :class="[activeStep == item.key ? 'active' : '']" @click="changeSteps(item)">
+            <span class="step-index">{{ index + 1 }}</span>
+            {{ item.label }}
+          </div>
+          <div class="ghost-step step" :style="{ transform: translateX }"></div>
+        </div>
+      </div>
+
+      <div class="my-nav-right">
+        <!-- <a href="https://gitee.com/ldhnet/AntFlow-Designer" target="_blank">
+        <svg t="1671270284061" class="icon" style="background-color: #fff;border-radius:20px;margin-bottom:-6px;"
+          viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1920" width="32" height="32">
+          <path
+            d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z"
+            fill="#C71D23" p-id="1921"></path>
+        </svg>
+      </a>
+      <a style="margin-right: 40px;" href="https://github.com/ldhnet/AntFlow-Designer" target="_blank">
+        <svg height="32" style="background-color: #fff;border-radius:20px;margin-bottom:-6px;" aria-hidden="true"
+          viewBox="0 0 24 24" version="1.1" width="32" data-view-component="true"
+          class="octicon octicon-mark-github v-align-middle">
+          <path
+            d="M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z">
+          </path>
+        </svg>
+      </a> -->
+        <button type="button" class="flow-btn button-publish" @click="publish">
+          <span>发 布</span>
+        </button>
+      </div>
+
+      <div v-if="processConfig" v-show="activeStep === 'basicSettingDesign'">
+        <basicSetting ref="basicSettingDesign" :basicData="processConfig" @nextChange="changeSteps" />
+      </div>
+      <div v-show="activeStep === 'formDesign'">
+        <dynamicForm ref="formDesign" />
+      </div>
+      <div v-if="nodeConfig" v-show="activeStep === 'processDesign'">
+        <process ref="processDesign" :processData="nodeConfig" @nextChange="changeSteps" />
+      </div>
+    </div>
+  </div>
+
+
+</template>
+
+
+<script>
+import { NodeUtils } from '@/utils/workflow/nodeUtils';
+import { FormatDisplayUtils } from '@/utils/workflow/formatdisplay_data';
+import { showLoading, closeLoading } from '@/utils/workflow/loading';
+import BasicSetting from './BasicSetting.vue';
+import DynamicForm from './DynamicForm/index.vue';
+import Process from './Process';
+export default {
+  components: {
+    BasicSetting,
+    DynamicForm,
+    Process
+  },
+  data() {
+    return {
+      title: '请假管理',
+      activeStep: 'basicSettingDesign',
+      steps: [
+        { label: "基础设置", key: "basicSettingDesign" },
+        { label: "表单设计", key: "formDesign" },
+        { label: "流程设计", key: "processDesign" }
+      ],
+      processConfig: null,
+      nodeConfig: null,
+      basicSettingDesign: null,
+      formDesign: null,
+      processDesign: null
+    }
+  },
+  computed: {
+    translateX() {
+      return `translateX(${(this.steps.findIndex(t => t.key === this.activeStep) - 1) * 100}%)`
+    }
+  },
+
+  mounted() {
+    // showLoading();
+    let mockjson = NodeUtils.createStartNode();
+    let data = FormatDisplayUtils.getToTree(mockjson.data);
+    this.processConfig = data;
+    this.title = data.bpmnName;
+    this.nodeConfig = data.nodeConfig;
+    // closeLoading();
+  },
+  methods: {
+    changeSteps(item) {
+      this.activeStep = item.key
+    },
+    publish() { }
+  }
+}
+
+</script>
+
+
+<style lang="scss" scoped>
+.step-tab {
+  display: flex;
+  justify-content: center;
+  position: relative;
+  height: 60px;
+  font-size: 14px;
+  border-right: 0px solid #1583f2;
+  text-align: center;
+  cursor: pointer;
+}
+
+.step {
+  width: 140px;
+  line-height: 100%;
+  padding-left: 30px;
+  padding-right: 30px;
+  line-height: 60px;
+  cursor: pointer;
+  position: relative;
+}
+
+.step-index {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  border: 1px solid #fff;
+  border-radius: 8px;
+  line-height: 18px;
+  text-align: center;
+  box-sizing: border-box;
+}
+
+.ghost-step {
+  position: absolute;
+  height: 60px;
+  z-index: -1;
+  background: #4483f2;
+  transition: transform .5s;
+}
+
+.ghost-step::after {
+  content: '';
+  border-width: 6px 6px 6px;
+  border-style: solid;
+  border-color: transparent transparent white;
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  margin-left: -6px;
+}
+</style>

+ 326 - 0
src/components/organization/DragOrg-12-11.vue

@@ -0,0 +1,326 @@
+<template>
+
+  <div >
+    <div class="my-nav-content1">
+      <section class="dingflow-design" ref="dingflowDesignRef">
+        <div class="zoom">
+          <div class="zoom-out" @click="zoomOut" title="缩小"></div>
+          <span>{{ nowVal }}%</span>
+          <div class="zoom-in" @click="zoomIn" title="放大"></div>
+          <!--刷新图标代码-->
+          <div class="zoom-reset" @click="zoomReset" title="还原缩放比例">&#10227</div>
+        </div>
+        <div class="box-scale" ref="boxScaleRef">
+          <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+            @on-expand="onExpand" />
+        </div>
+      </section>
+    </div>
+  </div>
+
+</template>
+
+
+
+<script>
+import { resetImage, wheelZoomFunc, zoomInit } from "@/utils/workflow/zoom.js";
+let id = 1000
+
+export default {
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      nowVal: 100,
+      // 初始数据
+      treeData: {                       
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部', positions: ['前端工程师', '后端工程师', 'DevOps'] },
+          { id: 3, label: '人事部' }
+        ]
+      },
+    }
+  },
+
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  mounted() {
+
+    this.$nextTick(() => {
+      if (!this.$refs.dingflowDesignRef || !this.$refs.boxScaleRef) return;
+      zoomInit(this.$refs.dingflowDesignRef, this.$refs.boxScaleRef, (val) => {
+        this.nowVal = val
+      })
+    });
+  },
+
+  methods: {
+
+    zoomIn() {
+      wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 + 0.1, isExternalCall: true })
+    },
+    zoomOut() {
+      wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 - 0.1, isExternalCall: true })
+    },
+    zoomReset() {
+      resetImage()
+    },
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label} ({data.positions ? data.positions.length : 0})
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+
+.my-nav-content1 {
+  width: 1200px;
+  height: 80%;
+  margin: 0 auto;
+  overflow: hidden;
+  position: relative;
+  position: fixed;
+  top: 140px;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 1;
+  overflow-x: hidden;
+  overflow-y: hidden;
+  padding-bottom: 30px;
+}
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+  box-sizing: border-box;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+/* 边界提示(调试用) */
+.boundary-debug {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 2px dashed red;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

+ 557 - 0
src/components/organization/DragOrg2Tree.vue

@@ -0,0 +1,557 @@
+<template>
+  <DraggableComp>
+
+    <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+      @on-expand="onExpand" />
+
+    
+  </DraggableComp>
+
+</template>
+
+
+
+<script>
+import DraggableComp from './DraggableComp.vue';
+let id = 1000
+export default {
+  components: { DraggableComp },
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      treeData: {                       // 初始数据
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部' },
+          { id: 3, label: '人事部' }
+        ]
+      },
+      // 缩放相关数据
+      zoom: 1, // 缩放比例,1表示100%
+      minZoom: 0.3, // 最小缩放比例
+      maxZoom: 3, // 最大缩放比例
+      zoomStep: 0.1, // 每次滚轮滚动的缩放步长
+      zoomOrigin: { x: 50, y: 50 }, // 缩放中心点,默认为中心
+
+      // 拖拽相关数据
+      isDragging: false,
+      dragStart: { x: 0, y: 0 },
+      position: { x: 0, y: 0 }, // 当前位置
+      lastPosition: { x: 0, y: 0 }, // 上次位置(用于连续拖拽)
+      boundary: {
+        minX: -Infinity,
+        maxX: Infinity,
+        minY: -Infinity,
+        maxY: Infinity
+      } // 拖拽边界
+    }
+  },
+
+  computed: {
+    // 计算容器样式(同时包含缩放和位置)
+    containerStyle() {
+      return {
+        transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.zoom})`,
+        transformOrigin: `${this.zoomOrigin.x}% ${this.zoomOrigin.y}%`,
+        transition: this.isDragging ? 'none' : 'transform 0.15s ease-out',
+        cursor: this.isDragging ? 'grabbing' : 'grab'
+      };
+    }
+  },
+
+
+  mounted() {
+    // 组件挂载后设置初始缩放中心和计算边界
+    this.updateZoomOrigin();
+    this.calculateBoundary();
+
+    // 添加全局鼠标事件监听
+    window.addEventListener('mousemove', this.handleDrag);
+    window.addEventListener('mouseup', this.stopDrag);
+
+    // 窗口大小改变时重新计算边界
+    window.addEventListener('resize', this.calculateBoundary);
+  },
+  beforeDestroy() {
+    // 移除事件监听
+    window.removeEventListener('mousemove', this.handleDrag);
+    window.removeEventListener('mouseup', this.stopDrag);
+    window.removeEventListener('resize', this.calculateBoundary);
+  },
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  methods: {
+
+    /* --------------- 拖拽相关方法 --------------- */
+    startDrag(event) {
+      // 防止触发其他事件(如文本选择)
+      event.preventDefault();
+
+      // 记录拖拽开始位置
+      this.isDragging = true;
+      this.dragStart = {
+        x: event.clientX - this.lastPosition.x,
+        y: event.clientY - this.lastPosition.y
+      };
+    },
+
+    handleDrag(event) {
+      if (!this.isDragging) return;
+
+      // 计算新的位置
+      let newX = event.clientX - this.dragStart.x;
+      let newY = event.clientY - this.dragStart.y;
+
+      // 限制在边界内
+      newX = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, newX));
+      newY = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, newY));
+
+      // 更新位置
+      this.position.x = newX;
+      this.position.y = newY;
+
+      // 保存最后位置
+      this.lastPosition.x = newX;
+      this.lastPosition.y = newY;
+    },
+
+    stopDrag() {
+      this.isDragging = false;
+    },
+
+    // 计算拖拽边界
+    calculateBoundary() {
+      const container = this.$refs.dragContainer;
+      const area = this.$refs.componentArea;
+
+      if (!container || !area) return;
+
+      const containerRect = container.getBoundingClientRect();
+      const areaRect = area.getBoundingClientRect();
+
+      // 计算边界(确保容器不会完全拖出可视区域)
+      const padding = 20; // 留出一些边距
+
+      this.boundary = {
+        minX: areaRect.left - containerRect.left + padding - (containerRect.width * (this.zoom - 1)) / 2,
+        maxX: areaRect.right - containerRect.right - padding + (containerRect.width * (this.zoom - 1)) / 2,
+        minY: areaRect.top - containerRect.top + padding - (containerRect.height * (this.zoom - 1)) / 2,
+        maxY: areaRect.bottom - containerRect.bottom - padding + (containerRect.height * (this.zoom - 1)) / 2
+      };
+    },
+
+    // 重置位置
+    resetPosition() {
+      this.position = { x: 0, y: 0 };
+      this.lastPosition = { x: 0, y: 0 };
+    },
+
+    /* --------------- 缩放相关方法 --------------- */
+    handleWheel(event) {
+      event.preventDefault();
+
+      // 计算鼠标在容器内的相对位置(作为缩放中心)
+      const container = this.$refs.dragContainer;
+      const rect = container.getBoundingClientRect();
+
+      const mouseXPercent = ((event.clientX - rect.left) / rect.width) * 100;
+      const mouseYPercent = ((event.clientY - rect.top) / rect.height) * 100;
+
+      // 更新缩放中心
+      this.zoomOrigin = {
+        x: Math.max(0, Math.min(100, mouseXPercent)),
+        y: Math.max(0, Math.min(100, mouseYPercent))
+      };
+
+      // 判断缩放方向
+      const delta = event.deltaY > 0 ? -1 : 1;
+
+      // 计算新缩放比例
+      let newZoom = this.zoom + (delta * this.zoomStep);
+
+      // 限制缩放范围
+      newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));
+
+      // 保存旧缩放值
+      const oldZoom = this.zoom;
+
+      // 应用缩放(保留一位小数)
+      this.zoom = Math.round(newZoom * 10) / 10;
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使鼠标所在点保持相对位置
+      if (oldZoom !== this.zoom) {
+        const zoomRatio = this.zoom / oldZoom;
+        const mouseX = event.clientX - rect.left;
+        const mouseY = event.clientY - rect.top;
+
+        this.position.x = mouseX - (mouseX - this.position.x) * zoomRatio;
+        this.position.y = mouseY - (mouseY - this.position.y) * zoomRatio;
+
+        // 限制在边界内
+        this.position.x = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, this.position.x));
+        this.position.y = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, this.position.y));
+
+        this.lastPosition.x = this.position.x;
+        this.lastPosition.y = this.position.y;
+      }
+    },
+
+    // 重置缩放
+    resetZoom() {
+      const oldZoom = this.zoom;
+      this.zoom = 1;
+      this.zoomOrigin = { x: 50, y: 50 };
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使组件居中
+      this.position.x = this.position.x / oldZoom;
+      this.position.y = this.position.y / oldZoom;
+
+      this.lastPosition.x = this.position.x;
+      this.lastPosition.y = this.position.y;
+    },
+
+    // 更新缩放中心为容器中心
+    updateZoomOrigin() {
+      this.zoomOrigin = { x: 50, y: 50 };
+    },
+
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label}
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+.page {
+  padding: 20px;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  /* 防止页面滚动 */
+}
+
+.toolbar {
+  margin-bottom: 20px;
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  flex-shrink: 0;
+  z-index: 10;
+  background: white;
+  padding: 10px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.zoom-hint {
+  margin-left: auto;
+  font-size: 12px;
+  color: #666;
+  font-weight: 500;
+}
+
+/* 组件区域 */
+.component-area {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  background: linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa),
+    linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa);
+  background-size: 20px 20px;
+  background-position: 0 0, 10px 10px;
+  min-height: 500px;
+}
+
+/* 拖拽和缩放容器 */
+.drag-zoom-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform-origin: center;
+  will-change: transform;
+  /* 优化性能 */
+  user-select: none;
+  /* 防止文本选择 */
+}
+
+/* 组织架构树包装器 */
+.org-tree-wrapper {
+  display: inline-block;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 30px;
+  position: relative;
+}
+
+/* 拖拽手柄 */
+.drag-handle {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  width: 32px;
+  height: 32px;
+  background: rgba(64, 158, 255, 0.9);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  color: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  z-index: 5;
+  transition: all 0.2s;
+}
+
+.drag-handle:hover {
+  background: rgba(64, 158, 255, 1);
+  transform: scale(1.1);
+}
+
+.drag-handle:active {
+  cursor: grabbing;
+  transform: scale(0.95);
+}
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+/* 边界提示(调试用) */
+.boundary-debug {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 2px dashed red;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 14 - 0
src/components/organization/DynamicForm/formRender.vue


+ 39 - 0
src/components/organization/DynamicForm/index.vue

@@ -0,0 +1,39 @@
+<template>
+    <div class="vform-content">
+        <v-form-designer ref="formDesign"></v-form-designer>
+    </div>
+</template>
+
+<script>
+
+export default {
+    methods: {
+        getData() {
+            let exportData = this.$refs.formDesign.getFormJson();
+            return new Promise((resolve, reject) => {
+                resolve({ formData: exportData })
+            })
+        }
+    }
+}
+
+
+</script>
+<style lang="scss" scoped>
+body {
+    margin: 0 !important;
+    /* 如果页面出现垂直滚动条,则加入此行CSS以消除之 */
+}
+
+.vform-content {
+    position: fixed;
+    top: 60px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    padding: 0 30px;
+    overflow-x: hidden;
+    overflow-y: auto;
+}
+</style>

+ 217 - 0
src/components/organization/EmployeeTable copy 3.vue

@@ -0,0 +1,217 @@
+<template>
+  <div id="app">
+    <h3>可缩放 + 可拖拽审批流</h3>
+
+    <!-- 工具条 -->
+    <div class="toolbar">
+
+      <div class="scale-wrap">
+        缩放:
+        <el-slider v-model="scale" :min="50" :max="150" :step="5" show-input input-size="mini"
+          style="width:200px;display:inline-block; vertical-align:middle; margin-left:10px;"></el-slider>
+      </div>
+    </div>
+    
+    <!-- 画布 -->
+    <div class="canvas" :style="{ transform: `scale(${scale / 100})` }" @wheel.ctrl.passive="onWheel">
+      <!-- 垂直链 -->
+      <div class="chain">
+        <!-- 渲染节点 -->
+        <div v-for="(node, i) in list" :key="node.id" class="node-box">
+          <!-- 节点 -->
+          <div class="node">
+            <span class="label">{{ node.name }}</span>
+            <!-- 减号:删节点 -->
+            <span class="btn del" @click="del(i)" v-if="!node.fixed">-</span>
+            <!-- 加号:往后插节点 -->
+            <span class="btn add" @click="add(i)">+</span>
+          </div>
+
+          <!-- 向下连线(最后一个不画) -->
+          <div class="line" v-if="i < list.length - 1"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 调试 -->
+    <pre class="debug">{{ JSON.stringify(list, null, 2) }}</pre>
+  </div>
+</template>
+
+
+<script>
+export default {
+  name: 'App',
+  data() {
+    return {
+      scale: 100,        // 缩放百分比
+      list: [
+        { id: 1, name: '开始', fixed: true },   // 固定节点,不可删
+        { id: 2, name: '结束', fixed: true }
+      ],
+      uid: 3,
+      dragIdx: null,     // 当前拖拽索引
+      dragStartX: 0,     // 拖拽起始偏移
+      dragStartY: 0
+    }
+  },
+  created() {
+    // 初始化 3 个节点
+    this.list = [
+      { id: this.uid++, name: '开始', x: 100, y: 50, fixed: true },
+      { id: this.uid++, name: '审批人', x: 100, y: 150 },
+      { id: this.uid++, name: '结束', x: 100, y: 250, fixed: true }
+    ]
+    // 如需要持久化,打开下面注释
+    // const cache = localStorage.getItem('flow')
+    // if (cache) this.list = JSON.parse(cache)
+  },
+  methods: {
+    /* 添加节点 */
+    // 在第 i 个节点“之后”插入新审批节点
+    add(i) {
+      this.list.splice(i + 1, 0, {
+        id: this.uid++,
+        name: '审批人',
+        fixed: false
+      })
+    },
+
+
+    del(i) {
+      this.list.splice(i, 1)
+    },
+
+    /* 拖拽 */
+    dragstart(e, i) {
+      this.dragIdx = i
+      const node = this.list[i]
+      const rect = e.target.getBoundingClientRect()
+      this.dragStartX = e.clientX - node.x * this.scale / 100
+      this.dragStartY = e.clientY - node.y * this.scale / 100
+      e.dataTransfer.effectAllowed = 'move'
+      // 隐藏默认幽灵
+      e.dataTransfer.setDragImage(new Image(), 0, 0)
+    },
+    dragend(e) {
+      const i = this.dragIdx
+      if (i === null) return
+      // 计算新坐标(除以 scale 得到画布内逻辑坐标)
+      this.list[i].x = (e.clientX - this.dragStartX) / (this.scale / 100)
+      this.list[i].y = (e.clientY - this.dragStartY) / (this.scale / 100)
+      this.dragIdx = null
+      // 持久化
+      // localStorage.setItem('flow', JSON.stringify(this.list))
+    },
+    /* 鼠标+Ctrl 缩放 */
+    onWheel(e) {
+      e.preventDefault()
+      const delta = e.deltaY > 0 ? -5 : 5
+      this.scale = Math.max(50, Math.min(150, this.scale + delta))
+    },
+    /* 构建 SVG 路径 */
+    buildPath(i) {
+      const a = this.list[i]
+      const b = this.list[i + 1]
+      const x1 = a.x + 60
+      const y1 = a.y + 40
+      const x2 = b.x + 60
+      const y2 = b.y
+      return `M${x1},${y1} C${x1},${y1 + 30} ${x2},${y2 - 30} ${x2},${y2}`
+    }
+  }
+}
+</script>
+
+<style scoped>
+.toolbar {
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.scale-wrap {
+  margin-left: auto;
+}
+
+.canvas {
+  position: relative;
+  width: 100%;
+  height: 70vh;
+  border: 1px solid #dcdfe6;
+  overflow: hidden;
+  transform-origin: 0 0;
+  background: #fafafa;
+}
+
+/* 垂直居中整个链 */
+.chain {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20px 0;
+}
+
+/* 单个节点盒子 */
+.node-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* 节点样式 */
+.node {
+  position: relative;
+  width: 140px;
+  line-height: 40px;
+  text-align: center;
+  border: 1px solid #409eff;
+  border-radius: 4px;
+  background: #fff;
+  cursor: pointer;
+}
+
+/* 加号/减号按钮 */
+.btn {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 20px;
+  height: 20px;
+  line-height: 18px;
+  text-align: center;
+  border-radius: 50%;
+  font-size: 16px;
+  color: #fff;
+  cursor: pointer;
+}
+
+.add {
+  background: #67c23a;
+  right: -30px;
+}
+
+.del {
+  background: #f56c6c;
+  left: -30px;
+}
+
+/* 向下连线 */
+.line {
+  width: 2px;
+  height: 40px;
+  background: #909399;
+  margin: 4px 0;
+}
+
+/* 调试用 */
+.debug {
+  background: #f5f7fa;
+  padding: 10px;
+  margin-top: 20px;
+  font-size: 12px;
+}
+
+
+</style>

+ 557 - 0
src/components/organization/EmployeeTable copy 4.vue

@@ -0,0 +1,557 @@
+<template>
+  <DraggableComp>
+
+    <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+      @on-expand="onExpand" />
+
+    
+  </DraggableComp>
+
+</template>
+
+
+
+<script>
+import DraggableComp from './DraggableComp.vue';
+let id = 1000
+export default {
+  components: { DraggableComp },
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      treeData: {                       // 初始数据
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部' },
+          { id: 3, label: '人事部' }
+        ]
+      },
+      // 缩放相关数据
+      zoom: 1, // 缩放比例,1表示100%
+      minZoom: 0.3, // 最小缩放比例
+      maxZoom: 3, // 最大缩放比例
+      zoomStep: 0.1, // 每次滚轮滚动的缩放步长
+      zoomOrigin: { x: 50, y: 50 }, // 缩放中心点,默认为中心
+
+      // 拖拽相关数据
+      isDragging: false,
+      dragStart: { x: 0, y: 0 },
+      position: { x: 0, y: 0 }, // 当前位置
+      lastPosition: { x: 0, y: 0 }, // 上次位置(用于连续拖拽)
+      boundary: {
+        minX: -Infinity,
+        maxX: Infinity,
+        minY: -Infinity,
+        maxY: Infinity
+      } // 拖拽边界
+    }
+  },
+
+  computed: {
+    // 计算容器样式(同时包含缩放和位置)
+    containerStyle() {
+      return {
+        transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.zoom})`,
+        transformOrigin: `${this.zoomOrigin.x}% ${this.zoomOrigin.y}%`,
+        transition: this.isDragging ? 'none' : 'transform 0.15s ease-out',
+        cursor: this.isDragging ? 'grabbing' : 'grab'
+      };
+    }
+  },
+
+
+  mounted() {
+    // 组件挂载后设置初始缩放中心和计算边界
+    this.updateZoomOrigin();
+    this.calculateBoundary();
+
+    // 添加全局鼠标事件监听
+    window.addEventListener('mousemove', this.handleDrag);
+    window.addEventListener('mouseup', this.stopDrag);
+
+    // 窗口大小改变时重新计算边界
+    window.addEventListener('resize', this.calculateBoundary);
+  },
+  beforeDestroy() {
+    // 移除事件监听
+    window.removeEventListener('mousemove', this.handleDrag);
+    window.removeEventListener('mouseup', this.stopDrag);
+    window.removeEventListener('resize', this.calculateBoundary);
+  },
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  methods: {
+
+    /* --------------- 拖拽相关方法 --------------- */
+    startDrag(event) {
+      // 防止触发其他事件(如文本选择)
+      event.preventDefault();
+
+      // 记录拖拽开始位置
+      this.isDragging = true;
+      this.dragStart = {
+        x: event.clientX - this.lastPosition.x,
+        y: event.clientY - this.lastPosition.y
+      };
+    },
+
+    handleDrag(event) {
+      if (!this.isDragging) return;
+
+      // 计算新的位置
+      let newX = event.clientX - this.dragStart.x;
+      let newY = event.clientY - this.dragStart.y;
+
+      // 限制在边界内
+      newX = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, newX));
+      newY = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, newY));
+
+      // 更新位置
+      this.position.x = newX;
+      this.position.y = newY;
+
+      // 保存最后位置
+      this.lastPosition.x = newX;
+      this.lastPosition.y = newY;
+    },
+
+    stopDrag() {
+      this.isDragging = false;
+    },
+
+    // 计算拖拽边界
+    calculateBoundary() {
+      const container = this.$refs.dragContainer;
+      const area = this.$refs.componentArea;
+
+      if (!container || !area) return;
+
+      const containerRect = container.getBoundingClientRect();
+      const areaRect = area.getBoundingClientRect();
+
+      // 计算边界(确保容器不会完全拖出可视区域)
+      const padding = 20; // 留出一些边距
+
+      this.boundary = {
+        minX: areaRect.left - containerRect.left + padding - (containerRect.width * (this.zoom - 1)) / 2,
+        maxX: areaRect.right - containerRect.right - padding + (containerRect.width * (this.zoom - 1)) / 2,
+        minY: areaRect.top - containerRect.top + padding - (containerRect.height * (this.zoom - 1)) / 2,
+        maxY: areaRect.bottom - containerRect.bottom - padding + (containerRect.height * (this.zoom - 1)) / 2
+      };
+    },
+
+    // 重置位置
+    resetPosition() {
+      this.position = { x: 0, y: 0 };
+      this.lastPosition = { x: 0, y: 0 };
+    },
+
+    /* --------------- 缩放相关方法 --------------- */
+    handleWheel(event) {
+      event.preventDefault();
+
+      // 计算鼠标在容器内的相对位置(作为缩放中心)
+      const container = this.$refs.dragContainer;
+      const rect = container.getBoundingClientRect();
+
+      const mouseXPercent = ((event.clientX - rect.left) / rect.width) * 100;
+      const mouseYPercent = ((event.clientY - rect.top) / rect.height) * 100;
+
+      // 更新缩放中心
+      this.zoomOrigin = {
+        x: Math.max(0, Math.min(100, mouseXPercent)),
+        y: Math.max(0, Math.min(100, mouseYPercent))
+      };
+
+      // 判断缩放方向
+      const delta = event.deltaY > 0 ? -1 : 1;
+
+      // 计算新缩放比例
+      let newZoom = this.zoom + (delta * this.zoomStep);
+
+      // 限制缩放范围
+      newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));
+
+      // 保存旧缩放值
+      const oldZoom = this.zoom;
+
+      // 应用缩放(保留一位小数)
+      this.zoom = Math.round(newZoom * 10) / 10;
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使鼠标所在点保持相对位置
+      if (oldZoom !== this.zoom) {
+        const zoomRatio = this.zoom / oldZoom;
+        const mouseX = event.clientX - rect.left;
+        const mouseY = event.clientY - rect.top;
+
+        this.position.x = mouseX - (mouseX - this.position.x) * zoomRatio;
+        this.position.y = mouseY - (mouseY - this.position.y) * zoomRatio;
+
+        // 限制在边界内
+        this.position.x = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, this.position.x));
+        this.position.y = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, this.position.y));
+
+        this.lastPosition.x = this.position.x;
+        this.lastPosition.y = this.position.y;
+      }
+    },
+
+    // 重置缩放
+    resetZoom() {
+      const oldZoom = this.zoom;
+      this.zoom = 1;
+      this.zoomOrigin = { x: 50, y: 50 };
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使组件居中
+      this.position.x = this.position.x / oldZoom;
+      this.position.y = this.position.y / oldZoom;
+
+      this.lastPosition.x = this.position.x;
+      this.lastPosition.y = this.position.y;
+    },
+
+    // 更新缩放中心为容器中心
+    updateZoomOrigin() {
+      this.zoomOrigin = { x: 50, y: 50 };
+    },
+
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label}
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+.page {
+  padding: 20px;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  /* 防止页面滚动 */
+}
+
+.toolbar {
+  margin-bottom: 20px;
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  flex-shrink: 0;
+  z-index: 10;
+  background: white;
+  padding: 10px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.zoom-hint {
+  margin-left: auto;
+  font-size: 12px;
+  color: #666;
+  font-weight: 500;
+}
+
+/* 组件区域 */
+.component-area {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  background: linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa),
+    linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa);
+  background-size: 20px 20px;
+  background-position: 0 0, 10px 10px;
+  min-height: 500px;
+}
+
+/* 拖拽和缩放容器 */
+.drag-zoom-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform-origin: center;
+  will-change: transform;
+  /* 优化性能 */
+  user-select: none;
+  /* 防止文本选择 */
+}
+
+/* 组织架构树包装器 */
+.org-tree-wrapper {
+  display: inline-block;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 30px;
+  position: relative;
+}
+
+/* 拖拽手柄 */
+.drag-handle {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  width: 32px;
+  height: 32px;
+  background: rgba(64, 158, 255, 0.9);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  color: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  z-index: 5;
+  transition: all 0.2s;
+}
+
+.drag-handle:hover {
+  background: rgba(64, 158, 255, 1);
+  transform: scale(1.1);
+}
+
+.drag-handle:active {
+  cursor: grabbing;
+  transform: scale(0.95);
+}
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+/* 边界提示(调试用) */
+.boundary-debug {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 2px dashed red;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

+ 535 - 0
src/components/organization/EmployeeTable copy 5.vue

@@ -0,0 +1,535 @@
+<template>
+  <grabbing-box :init-scale="100" :max-scale="200" :min-scale="50" :throttle-span="100">
+    <!-- 任意需要拖拽/缩放的内容 -->
+    <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+      @on-expand="onExpand" />
+  </grabbing-box>
+
+</template>
+
+
+
+<script>
+export default {
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      treeData: {                       // 初始数据
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部' },
+          { id: 3, label: '人事部' }
+        ]
+      },
+      
+    }
+  },
+
+  computed: {
+    // 计算容器样式(同时包含缩放和位置)
+    containerStyle() {
+      return {
+        transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.zoom})`,
+        transformOrigin: `${this.zoomOrigin.x}% ${this.zoomOrigin.y}%`,
+        transition: this.isDragging ? 'none' : 'transform 0.15s ease-out',
+        cursor: this.isDragging ? 'grabbing' : 'grab'
+      };
+    }
+  },
+
+
+  mounted() {
+    // 组件挂载后设置初始缩放中心和计算边界
+    this.updateZoomOrigin();
+    this.calculateBoundary();
+
+    // 添加全局鼠标事件监听
+    window.addEventListener('mousemove', this.handleDrag);
+    window.addEventListener('mouseup', this.stopDrag);
+
+    // 窗口大小改变时重新计算边界
+    window.addEventListener('resize', this.calculateBoundary);
+  },
+  beforeDestroy() {
+    // 移除事件监听
+    window.removeEventListener('mousemove', this.handleDrag);
+    window.removeEventListener('mouseup', this.stopDrag);
+    window.removeEventListener('resize', this.calculateBoundary);
+  },
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  methods: {
+
+    /* --------------- 拖拽相关方法 --------------- */
+    startDrag(event) {
+      // 防止触发其他事件(如文本选择)
+      event.preventDefault();
+
+      // 记录拖拽开始位置
+      this.isDragging = true;
+      this.dragStart = {
+        x: event.clientX - this.lastPosition.x,
+        y: event.clientY - this.lastPosition.y
+      };
+    },
+
+    handleDrag(event) {
+      if (!this.isDragging) return;
+
+      // 计算新的位置
+      let newX = event.clientX - this.dragStart.x;
+      let newY = event.clientY - this.dragStart.y;
+
+      // 限制在边界内
+      newX = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, newX));
+      newY = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, newY));
+
+      // 更新位置
+      this.position.x = newX;
+      this.position.y = newY;
+
+      // 保存最后位置
+      this.lastPosition.x = newX;
+      this.lastPosition.y = newY;
+    },
+
+    stopDrag() {
+      this.isDragging = false;
+    },
+
+    // 计算拖拽边界
+    calculateBoundary() {
+      const container = this.$refs.dragContainer;
+      const area = this.$refs.componentArea;
+
+      if (!container || !area) return;
+
+      const containerRect = container.getBoundingClientRect();
+      const areaRect = area.getBoundingClientRect();
+
+      // 计算边界(确保容器不会完全拖出可视区域)
+      const padding = 20; // 留出一些边距
+
+      this.boundary = {
+        minX: areaRect.left - containerRect.left + padding - (containerRect.width * (this.zoom - 1)) / 2,
+        maxX: areaRect.right - containerRect.right - padding + (containerRect.width * (this.zoom - 1)) / 2,
+        minY: areaRect.top - containerRect.top + padding - (containerRect.height * (this.zoom - 1)) / 2,
+        maxY: areaRect.bottom - containerRect.bottom - padding + (containerRect.height * (this.zoom - 1)) / 2
+      };
+    },
+
+    // 重置位置
+    resetPosition() {
+      this.position = { x: 0, y: 0 };
+      this.lastPosition = { x: 0, y: 0 };
+    },
+
+    /* --------------- 缩放相关方法 --------------- */
+    handleWheel(event) {
+      event.preventDefault();
+
+      // 计算鼠标在容器内的相对位置(作为缩放中心)
+      const container = this.$refs.dragContainer;
+      const rect = container.getBoundingClientRect();
+
+      const mouseXPercent = ((event.clientX - rect.left) / rect.width) * 100;
+      const mouseYPercent = ((event.clientY - rect.top) / rect.height) * 100;
+
+      // 更新缩放中心
+      this.zoomOrigin = {
+        x: Math.max(0, Math.min(100, mouseXPercent)),
+        y: Math.max(0, Math.min(100, mouseYPercent))
+      };
+
+      // 判断缩放方向
+      const delta = event.deltaY > 0 ? -1 : 1;
+
+      // 计算新缩放比例
+      let newZoom = this.zoom + (delta * this.zoomStep);
+
+      // 限制缩放范围
+      newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));
+
+      // 保存旧缩放值
+      const oldZoom = this.zoom;
+
+      // 应用缩放(保留一位小数)
+      this.zoom = Math.round(newZoom * 10) / 10;
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使鼠标所在点保持相对位置
+      if (oldZoom !== this.zoom) {
+        const zoomRatio = this.zoom / oldZoom;
+        const mouseX = event.clientX - rect.left;
+        const mouseY = event.clientY - rect.top;
+
+        this.position.x = mouseX - (mouseX - this.position.x) * zoomRatio;
+        this.position.y = mouseY - (mouseY - this.position.y) * zoomRatio;
+
+        // 限制在边界内
+        this.position.x = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, this.position.x));
+        this.position.y = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, this.position.y));
+
+        this.lastPosition.x = this.position.x;
+        this.lastPosition.y = this.position.y;
+      }
+    },
+
+    // 重置缩放
+    resetZoom() {
+      const oldZoom = this.zoom;
+      this.zoom = 1;
+      this.zoomOrigin = { x: 50, y: 50 };
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使组件居中
+      this.position.x = this.position.x / oldZoom;
+      this.position.y = this.position.y / oldZoom;
+
+      this.lastPosition.x = this.position.x;
+      this.lastPosition.y = this.position.y;
+    },
+
+    // 更新缩放中心为容器中心
+    updateZoomOrigin() {
+      this.zoomOrigin = { x: 50, y: 50 };
+    },
+
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label}
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+.page {
+  padding: 20px;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  /* 防止页面滚动 */
+}
+
+.toolbar {
+  margin-bottom: 20px;
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  flex-shrink: 0;
+  z-index: 10;
+  background: white;
+  padding: 10px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.zoom-hint {
+  margin-left: auto;
+  font-size: 12px;
+  color: #666;
+  font-weight: 500;
+}
+
+/* 组件区域 */
+.component-area {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  background: linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa),
+    linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa);
+  background-size: 20px 20px;
+  background-position: 0 0, 10px 10px;
+  min-height: 500px;
+}
+
+/* 拖拽和缩放容器 */
+.drag-zoom-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform-origin: center;
+  will-change: transform;
+  /* 优化性能 */
+  user-select: none;
+  /* 防止文本选择 */
+}
+
+/* 组织架构树包装器 */
+.org-tree-wrapper {
+  display: inline-block;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 30px;
+  position: relative;
+}
+
+/* 拖拽手柄 */
+.drag-handle {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  width: 32px;
+  height: 32px;
+  background: rgba(64, 158, 255, 0.9);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  color: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  z-index: 5;
+  transition: all 0.2s;
+}
+
+.drag-handle:hover {
+  background: rgba(64, 158, 255, 1);
+  transform: scale(1.1);
+}
+
+.drag-handle:active {
+  cursor: grabbing;
+  transform: scale(0.95);
+}
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+/* 边界提示(调试用) */
+.boundary-debug {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 2px dashed red;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

+ 557 - 0
src/components/organization/EmployeeTable copy 6.vue

@@ -0,0 +1,557 @@
+<template>
+  <DraggableComp>
+
+    <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+      @on-expand="onExpand" />
+
+    
+  </DraggableComp>
+
+</template>
+
+
+
+<script>
+import DraggableComp from './DraggableComp.vue';
+let id = 1000
+export default {
+  components: { DraggableComp },
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      treeData: {                       // 初始数据
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部' },
+          { id: 3, label: '人事部' }
+        ]
+      },
+      // 缩放相关数据
+      zoom: 1, // 缩放比例,1表示100%
+      minZoom: 0.3, // 最小缩放比例
+      maxZoom: 3, // 最大缩放比例
+      zoomStep: 0.1, // 每次滚轮滚动的缩放步长
+      zoomOrigin: { x: 50, y: 50 }, // 缩放中心点,默认为中心
+
+      // 拖拽相关数据
+      isDragging: false,
+      dragStart: { x: 0, y: 0 },
+      position: { x: 0, y: 0 }, // 当前位置
+      lastPosition: { x: 0, y: 0 }, // 上次位置(用于连续拖拽)
+      boundary: {
+        minX: -Infinity,
+        maxX: Infinity,
+        minY: -Infinity,
+        maxY: Infinity
+      } // 拖拽边界
+    }
+  },
+
+  computed: {
+    // 计算容器样式(同时包含缩放和位置)
+    containerStyle() {
+      return {
+        transform: `translate(${this.position.x}px, ${this.position.y}px) scale(${this.zoom})`,
+        transformOrigin: `${this.zoomOrigin.x}% ${this.zoomOrigin.y}%`,
+        transition: this.isDragging ? 'none' : 'transform 0.15s ease-out',
+        cursor: this.isDragging ? 'grabbing' : 'grab'
+      };
+    }
+  },
+
+
+  mounted() {
+    // 组件挂载后设置初始缩放中心和计算边界
+    this.updateZoomOrigin();
+    this.calculateBoundary();
+
+    // 添加全局鼠标事件监听
+    window.addEventListener('mousemove', this.handleDrag);
+    window.addEventListener('mouseup', this.stopDrag);
+
+    // 窗口大小改变时重新计算边界
+    window.addEventListener('resize', this.calculateBoundary);
+  },
+  beforeDestroy() {
+    // 移除事件监听
+    window.removeEventListener('mousemove', this.handleDrag);
+    window.removeEventListener('mouseup', this.stopDrag);
+    window.removeEventListener('resize', this.calculateBoundary);
+  },
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  methods: {
+
+    /* --------------- 拖拽相关方法 --------------- */
+    startDrag(event) {
+      // 防止触发其他事件(如文本选择)
+      event.preventDefault();
+
+      // 记录拖拽开始位置
+      this.isDragging = true;
+      this.dragStart = {
+        x: event.clientX - this.lastPosition.x,
+        y: event.clientY - this.lastPosition.y
+      };
+    },
+
+    handleDrag(event) {
+      if (!this.isDragging) return;
+
+      // 计算新的位置
+      let newX = event.clientX - this.dragStart.x;
+      let newY = event.clientY - this.dragStart.y;
+
+      // 限制在边界内
+      newX = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, newX));
+      newY = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, newY));
+
+      // 更新位置
+      this.position.x = newX;
+      this.position.y = newY;
+
+      // 保存最后位置
+      this.lastPosition.x = newX;
+      this.lastPosition.y = newY;
+    },
+
+    stopDrag() {
+      this.isDragging = false;
+    },
+
+    // 计算拖拽边界
+    calculateBoundary() {
+      const container = this.$refs.dragContainer;
+      const area = this.$refs.componentArea;
+
+      if (!container || !area) return;
+
+      const containerRect = container.getBoundingClientRect();
+      const areaRect = area.getBoundingClientRect();
+
+      // 计算边界(确保容器不会完全拖出可视区域)
+      const padding = 20; // 留出一些边距
+
+      this.boundary = {
+        minX: areaRect.left - containerRect.left + padding - (containerRect.width * (this.zoom - 1)) / 2,
+        maxX: areaRect.right - containerRect.right - padding + (containerRect.width * (this.zoom - 1)) / 2,
+        minY: areaRect.top - containerRect.top + padding - (containerRect.height * (this.zoom - 1)) / 2,
+        maxY: areaRect.bottom - containerRect.bottom - padding + (containerRect.height * (this.zoom - 1)) / 2
+      };
+    },
+
+    // 重置位置
+    resetPosition() {
+      this.position = { x: 0, y: 0 };
+      this.lastPosition = { x: 0, y: 0 };
+    },
+
+    /* --------------- 缩放相关方法 --------------- */
+    handleWheel(event) {
+      event.preventDefault();
+
+      // 计算鼠标在容器内的相对位置(作为缩放中心)
+      const container = this.$refs.dragContainer;
+      const rect = container.getBoundingClientRect();
+
+      const mouseXPercent = ((event.clientX - rect.left) / rect.width) * 100;
+      const mouseYPercent = ((event.clientY - rect.top) / rect.height) * 100;
+
+      // 更新缩放中心
+      this.zoomOrigin = {
+        x: Math.max(0, Math.min(100, mouseXPercent)),
+        y: Math.max(0, Math.min(100, mouseYPercent))
+      };
+
+      // 判断缩放方向
+      const delta = event.deltaY > 0 ? -1 : 1;
+
+      // 计算新缩放比例
+      let newZoom = this.zoom + (delta * this.zoomStep);
+
+      // 限制缩放范围
+      newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));
+
+      // 保存旧缩放值
+      const oldZoom = this.zoom;
+
+      // 应用缩放(保留一位小数)
+      this.zoom = Math.round(newZoom * 10) / 10;
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使鼠标所在点保持相对位置
+      if (oldZoom !== this.zoom) {
+        const zoomRatio = this.zoom / oldZoom;
+        const mouseX = event.clientX - rect.left;
+        const mouseY = event.clientY - rect.top;
+
+        this.position.x = mouseX - (mouseX - this.position.x) * zoomRatio;
+        this.position.y = mouseY - (mouseY - this.position.y) * zoomRatio;
+
+        // 限制在边界内
+        this.position.x = Math.max(this.boundary.minX, Math.min(this.boundary.maxX, this.position.x));
+        this.position.y = Math.max(this.boundary.minY, Math.min(this.boundary.maxY, this.position.y));
+
+        this.lastPosition.x = this.position.x;
+        this.lastPosition.y = this.position.y;
+      }
+    },
+
+    // 重置缩放
+    resetZoom() {
+      const oldZoom = this.zoom;
+      this.zoom = 1;
+      this.zoomOrigin = { x: 50, y: 50 };
+
+      // 缩放后重新计算边界
+      this.calculateBoundary();
+
+      // 调整位置,使组件居中
+      this.position.x = this.position.x / oldZoom;
+      this.position.y = this.position.y / oldZoom;
+
+      this.lastPosition.x = this.position.x;
+      this.lastPosition.y = this.position.y;
+    },
+
+    // 更新缩放中心为容器中心
+    updateZoomOrigin() {
+      this.zoomOrigin = { x: 50, y: 50 };
+    },
+
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label}
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+.page {
+  padding: 20px;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  /* 防止页面滚动 */
+}
+
+.toolbar {
+  margin-bottom: 20px;
+  display: flex;
+  gap: 15px;
+  align-items: center;
+  flex-shrink: 0;
+  z-index: 10;
+  background: white;
+  padding: 10px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.zoom-hint {
+  margin-left: auto;
+  font-size: 12px;
+  color: #666;
+  font-weight: 500;
+}
+
+/* 组件区域 */
+.component-area {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  background: linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa),
+    linear-gradient(45deg, #fafafa 25%, transparent 25%, transparent 75%, #fafafa 75%, #fafafa);
+  background-size: 20px 20px;
+  background-position: 0 0, 10px 10px;
+  min-height: 500px;
+}
+
+/* 拖拽和缩放容器 */
+.drag-zoom-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform-origin: center;
+  will-change: transform;
+  /* 优化性能 */
+  user-select: none;
+  /* 防止文本选择 */
+}
+
+/* 组织架构树包装器 */
+.org-tree-wrapper {
+  display: inline-block;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 30px;
+  position: relative;
+}
+
+/* 拖拽手柄 */
+.drag-handle {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  width: 32px;
+  height: 32px;
+  background: rgba(64, 158, 255, 0.9);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  color: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  z-index: 5;
+  transition: all 0.2s;
+}
+
+.drag-handle:hover {
+  background: rgba(64, 158, 255, 1);
+  transform: scale(1.1);
+}
+
+.drag-handle:active {
+  cursor: grabbing;
+  transform: scale(0.95);
+}
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+/* 边界提示(调试用) */
+.boundary-debug {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 2px dashed red;
+  pointer-events: none;
+  z-index: 100;
+}
+</style>

+ 299 - 0
src/components/organization/EmployeeTable copy.vue

@@ -0,0 +1,299 @@
+<template>
+
+  <div >
+    <div class="my-nav-content">
+      <section class="dingflow-design" ref="dingflowDesignRef">
+        <div class="zoom">
+          <div class="zoom-out" @click="zoomOut" title="缩小"></div>
+          <span>{{ nowVal }}%</span>
+          <div class="zoom-in" @click="zoomIn" title="放大"></div>
+          <!--刷新图标代码-->
+          <div class="zoom-reset" @click="zoomReset" title="还原缩放比例">&#10227</div>
+        </div>
+        <div class="box-scale" ref="boxScaleRef">
+          <vue2-org-tree :data="treeData" :props="defaultProps" collapsable :render-content="renderNode"
+            @on-expand="onExpand" />
+        </div>
+      </section>
+    </div>
+  </div>
+
+</template>
+
+
+
+<script>
+import { resetImage, wheelZoomFunc, zoomInit } from "@/utils/workflow/zoom.js";
+let id = 1000
+
+export default {
+  data() {
+    return {
+      defaultProps: { label: 'label', children: 'children', expand: 'expand' },
+      nowVal: 100,
+      // 初始数据
+      treeData: {                       
+        id: 0,
+        label: '广东功道云科技有限公司',
+        expand: true,
+        children: [
+          { id: 1, label: '研发部', expand: true, positions: ['前端工程师', '后端工程师', 'DevOps'], children: [{ id: 4, label: '前端组' }, { id: 5, label: '后端组' }] },
+          { id: 2, label: '销售部' },
+          { id: 3, label: '人事部' }
+        ]
+      },
+    }
+  },
+
+
+  created() {
+    // 初始化职位显示状态
+    this.initPositionVisibility(this.treeData);
+  },
+
+  mounted() {
+
+    this.$nextTick(() => {
+      if (!this.$refs.dingflowDesignRef || !this.$refs.boxScaleRef) return;
+      zoomInit(this.$refs.dingflowDesignRef, this.$refs.boxScaleRef, (val) => {
+        this.nowVal = val
+      })
+    });
+  },
+
+  methods: {
+
+    zoomIn() {
+      wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 + 0.1, isExternalCall: true })
+    },
+    zoomOut() {
+      wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 - 0.1, isExternalCall: true })
+    },
+    zoomReset() {
+      resetImage()
+    },
+
+    initPositionVisibility(data) {
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', true);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.initPositionVisibility(child));
+      }
+    },
+    /* --------------- 节点渲染 --------------- */
+    renderNode(h, data) {
+      // if (!data._showPos) this.$set(data, '_showPos', true)   // 折叠状态
+
+      return (
+        <div class="node-wrapper">
+          {/* 1. 折叠按钮 —— 在 popover 之外 */}
+
+
+          {/* 2. 原 popover 包裹的节点主体 */}
+          <el-popover placement="bottom" width="260" trigger="click">
+            <div style="text-align:center;">
+              <el-button size="mini" type="primary" onClick={() => this.addChild(data)}>添加子部门</el-button>
+              <el-button size="mini" onClick={() => this.editNode(data)}>编辑</el-button>
+              <el-button size="mini" type="danger" onClick={() => this.delNode(data)}>删除</el-button>
+            </div>
+            <div slot="reference" class="node-body">
+              <div class="node-label">
+                {data.label}
+                {data.positions && data.positions.length > 0 && (
+                  <span
+                    class={['fold-btn', { 'collapsed': !data._showPos }]}
+                    onClick={e => { e.stopPropagation(); data._showPos = !data._showPos }}>
+                    {data._showPos ? '▲' : '▼'}
+                  </span>
+                )}
+              </div>
+              {/* 职位列表 */}
+              {data._showPos && data.positions && data.positions.length > 0 && (
+                <ul class="pos-list">
+                  {data.positions.map(p => <li key={p}>{p}</li>)}
+                </ul>
+              )}
+            </div>
+          </el-popover>
+        </div>
+      )
+    },
+
+    /* --------------- 业务函数 --------------- */
+    addChild(parent) {                      // 添加子节点
+      this.$prompt('请输入新部门名称', '添加子部门', {
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        if (!parent.children) this.$set(parent, 'children', [])
+        parent.children.push({ id: ++id, label: value.trim(), expand: false })
+        this.$message.success('添加成功')
+      })
+    },
+    editNode(node) {                        // 编辑节点
+      this.$prompt('请输入新的部门名称', '编辑部门', {
+        inputValue: node.label,
+        inputValidator: v => v && v.trim().length > 0
+      }).then(({ value }) => {
+        this.$set(node, 'label', value.trim())
+        this.$message.success('修改成功')
+      })
+    },
+    delNode(node) {                         // 删除节点
+      this.$confirm(`确定删除 “${node.label}” 及其所有子部门吗?`, '提示', { type: 'warning' })
+        .then(() => {
+          const parent = this.findParent(this.treeData, node)
+          if (!parent) return                      // 根节点不让删
+          const idx = parent.children.indexOf(node)
+          parent.children.splice(idx, 1)
+          this.$message.success('删除成功')
+        })
+    },
+
+    /* --------------- 辅助函数 --------------- */
+    findParent(root, target, parent = null) {      // 递归找父节点
+      if (root === target) return parent
+      if (root.children) {
+        for (const child of root.children) {
+          const res = this.findParent(child, target, root)
+          if (res) return res
+        }
+      }
+      return null
+    },
+    // 展开/收起全部
+    expandAll(val) {
+      // 同时切换所有节点的职位列表显示状态
+      this.togglePositionVisibility(this.treeData, val);
+    },
+
+    toggleExpand(data, val) {
+      this.$set(data, 'expand', val)
+      if (data.children) data.children.forEach(c => this.toggleExpand(c, val))
+    },
+
+    togglePositionVisibility(data, val) {
+      // 只有有职位的节点才设置 _showPos
+      if (data.positions && data.positions.length > 0) {
+        this.$set(data, '_showPos', val);
+      }
+      if (data.children) {
+        data.children.forEach(child => this.togglePositionVisibility(child, val));
+      }
+    },
+
+    collapse(list) {
+      var that = this;
+      list.forEach(function (child) {
+        if (child.expand) {
+          child.expand = false;
+        }
+        child.children && that.collapse(child.children);
+      });
+    },
+
+    // 折叠回调
+    onExpand(e, data) {
+      console.log('expand change:', data.label, data.expand)
+      if ("expand" in data) {
+        data.expand = !data.expand;
+        if (!data.expand && data.children) {
+          this.collapse(data.children);
+        }
+      } else {
+        this.$set(data, "expand", true);
+      }
+    }
+  }
+}
+</script>
+
+
+<style lang="scss" scoped>
+
+
+/* 节点相关样式保持不变 */
+.node-label {
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 3px;
+}
+
+.node-label:hover {
+  background: #f5f7fa;
+}
+
+.node-body {
+  display: inline-block;
+  text-align: center;
+  padding: 6px 10px;
+  background: #fff;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+}
+
+.node-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.node-label {
+  font-weight: 500;
+}
+
+.pos-list {
+  list-style: none;
+  margin: 4px 0 0 0;
+  padding: 0;
+  font-size: 12px;
+  color: #666;
+  text-align: left;
+}
+
+.pos-list li {
+  padding: 2px 0;
+}
+
+.pos-list li::before {
+  content: "· ";
+  font-weight: bold;
+  color: #409EFF;
+}
+
+.node-wrapper {
+  display: inline-flex;
+  align-items: flex-start;
+  gap: 4px;
+}
+
+.fold-btn {
+  cursor: pointer;
+  font-size: 12px;
+  color: #409EFF;
+  user-select: none;
+  padding: 2px 4px;
+  box-sizing: border-box;
+  transition: color 0.2s;
+}
+
+.fold-btn.collapsed {
+  color: #909399;
+}
+
+/* 缩放比例指示器(浮动) */
+.zoom-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  z-index: 10;
+  pointer-events: none;
+}
+
+
+</style>

+ 1761 - 0
src/components/organization/EmployeeTable-master.vue

@@ -0,0 +1,1761 @@
+<template>
+  <div>
+    <el-alert class="diy-tip" title="温馨提示:" @close="tips_close" v-show="tips_show" type="success" description show-icon>
+      <p>第一步:添加部门</p>
+      <p>第二步:新增员工(员工可以批量导入,每次最多可以导入200个员工)</p>
+      <p>备注:(导入员工的数量不能大于购买的员工数量)</p>
+    </el-alert>
+
+    <div class="flex-box all-box br-5" style="display: flex !important;">
+      <div class="RuleLeft box-sizing-w scroll-bar">
+        <el-button @click="addTheDepartment" type="primary" style="margin-bottom: 10px;width:164px;height:36px;text-align: center;font-size:14px;">+添加部门</el-button>
+        <div v-loading="dept_show">
+          <div @click="handleNodeClick({ value: 0 })" class="company_name">
+            <img src="static/images/two.png" />
+            <span>{{ firstCompany.name }}</span>
+          </div>
+          <div ref="ruletree" class="department_box">
+            <el-tree
+              ref="menum"
+              v-show="treedata !== null && treedata.length != 0"
+              node-key="id"
+              :accordion="true"
+              :highlight-current="true"
+              @node-expand="treeOpen"
+              :data="treedata"
+              :default-expand-all="defaultExpand"
+              :props="defaultProps"
+              @node-click="handleNodeClick"
+            >
+              <div
+                content="tree"
+                v-show="treedata.length != 0"
+                class="custom-tree-node"
+                slot-scope="{ node, data }"
+                style="font-size: 14px;color: #606266; width:100%; text-align: left;"
+              >
+                <img src="static/images/one.png" />
+                <span class="name">{{ data.name }}</span>
+              </div>
+            </el-tree>
+            <div v-show="treedata !== null && treedata.length == 0 && !dept_show">
+              <p style="padding: 0 20px;margin-top: 50px;"><img src="static/images/nodata_default.png" width="100%" alt="" /></p>
+              <p style="text-align: center;margin: 50px 0;">
+                目前没有部门,去
+                <span @click="addTheDepartment" style="color: #409EFF;">添加</span>
+                吧
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="RuleRight flex-1" v-loading="loading">
+        <div class="title">
+          <span class="sapn">{{ company.name }}</span>
+          (
+          <span>{{ nowEmploye }}人</span>
+          )
+          <el-button type="primary" @click="openEmploe" v-if="set_btn" style="margin-left:10px;">编辑</el-button>
+        </div>
+        <div class="flex-box-end flex-v-ce" style="margin-top: 20px;">
+          <div class="flex-1">
+          <div class="green" v-if="company.pid == undefined">
+<!--              创始人:
+              <el-tag :key="index" v-for="(item, index) in supermanger" :disable-transitions="false">{{ item.name }}</el-tag> -->
+          </div>
+          <template v-else>
+            <div class="green">
+              <span>管理员:</span>
+              <el-tag :key="index" v-for="(item, index) in supermanger" closable :disable-transitions="false" @close="deleteEmployee(item)">{{ item.name }}</el-tag>
+              <el-button size="mini" type="primary" icon="el-icon-plus" plain @click="show_employee_selector = true"></el-button>
+            </div>
+            <div class="fontColorD" style="padding: 0 30px;padding-top: 10px;">只能选择直属部门的员工作为部门管理员</div>
+          </template>
+          </div>
+          <div class="blue cursor" style="padding-right: 20px;" @click="$openUrl(6)"><i class="el-icon-question" style="padding-right: 5px;font-size: 16px;position: relative;top: 1px;"></i>如何邀请员工注册</div>
+          <el-button type="primary" @click="new_employee" class="fr">新成员列表</el-button>
+          <el-button type="danger" plain @click="isShowDelete=true" class="fr">离职人员列表</el-button>
+        </div>
+
+        <div class="list_box">
+          <div style="margin-bottom:10px;margin-top:30px;" class="inline-block-btn-list">
+            <!-- <el-button class="first-element-btn" :disabled="multipleSelection.length == 0" @click="confirmMultipleDelete" type="danger">批量删除</el-button> -->
+            <el-button @click="EmployeeAdd" type="primary">新增员工</el-button>
+            <el-button @click="dialogExcelVisible = true">导入员工</el-button>
+            <el-button @click="exportEmploye = true">导出/修改员工</el-button>
+
+
+            <el-checkbox v-model="account_id" style="margin-left: 10px;">仅显示未注册</el-checkbox>
+            <div class="fr" style="display:inline-block; width:250px;">
+              <el-input placeholder="请输入名字或者手机号码" clearable v-model="filter.keywords"  maxlength="20"></el-input>
+            </div>
+          </div>
+
+          <el-table @filter-change="filterHandler" ref="multipleTable" :data="tableData" tooltip-effect="dark" @selection-change="handleSelectionChange" @row-click="toDetailPage">
+            <!-- <el-table-column type="selection" width="50" :selectable="isSelectable"></el-table-column> -->
+            <el-table-column prop="name" label="姓名">
+              <template slot-scope="scope">
+                <userImage class="fl" :id="scope.row.id" :user_name="scope.row.name" :img_url="scope.row.img_url" fontSize="16" width="50px" height="50px"></userImage>
+                <span style="margin-left: 10px; line-height: 50px; display: inline-block;">{{ scope.row.name }}</span>
+                <span class="administration" v-if="scope.row.employee_detail.manage_dept_ids.length != 0">管</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="dept_list" label="部门">
+              <template slot-scope="scope">
+                <template v-if="scope.row.employee_detail.dept_list.length > 0">
+                  <span v-for="(item, index) in scope.row.employee_detail.dept_list" :key="index">
+                    {{ item.dept_name }}
+                    <span v-if="scope.row.employee_detail.dept_list.length - index > 1">,</span>
+                  </span>
+                </template>
+                <span v-else>--</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="tel" label="电话"></el-table-column>
+            <el-table-column prop="post" label="岗位">
+              <template slot-scope="scope">
+                  <el-link v-if="scope.row.post" @click.stop="openPost(scope.row)">{{scope.row.post}}</el-link>
+                  <el-link type="primary" v-else @click.stop="openPost(scope.row)">设置</el-link>
+              </template>
+            </el-table-column>
+            <el-table-column prop="status" width="120px" align="center">
+
+              <template slot="header" slot-scope="scope">
+                <el-select v-model="status" size="mini" class="selectVal">
+                  <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </el-select>
+              </template>
+
+              <template slot-scope="scope">
+                <div v-if="scope.row.status == 0" style="color: rgb(255 168 90);">待加入</div>
+                <div v-if="scope.row.status == 1 && scope.row.account_id >= 1" style="color: #409EFF;">在职</div>
+                <div v-if="scope.row.status == 1 && scope.row.account_id == 0" style="color: #409EFF;">
+                  在职
+                  <span class="red">(未注册)</span>
+                </div>
+                <div v-if="scope.row.status == 2" style="color: #f56c6c;">离职</div>
+                <div v-if="scope.row.status == 3" style="color: rgb(107 204 131);cursor:pointer" @click.stop="againInvite(scope.row)">再次邀请</div>
+              </template>
+            </el-table-column>
+
+            <el-table-column prop="accedence_time" label="入职日期" align="center"></el-table-column>
+            <template slot="empty">
+              <NoData></NoData>
+            </template>
+          </el-table>
+            <el-pagination
+              background
+              layout="total,sizes, prev, pager, next"
+              @size-change="handleSizeChange"
+              :page-size="pageLimit"
+              :page-sizes="[10, 20, 50, 100]"
+              :total="totalCount"
+              :current-page.sync="currentPage"
+              @current-change="changePage"
+            ></el-pagination>
+        </div>
+      </div>
+    </div>
+
+    <!-- 导入员工 -->
+    <el-dialog title="导入员工" width="600px" :visible.sync="dialogExcelVisible" :close-on-click-modal="false" :before-close="close_import">
+      <div style="padding:0 50px;">
+        <div class="align-center" style="margin-bottom:20px;">
+          <!-- <p>温馨提示:</p> -->
+          <p>
+            1、建立公司组织架构
+            <span><span style="display: inline-block; height:36px;"></span></span>
+          </p>
+          <p>
+            2、下载员工信息表模版,批量填写员工信息
+            <a target="_blank" :href="$serverdomain + '/excel/员工新增导入模板.xlsx'"><el-button type="primary" plain>下载模板</el-button></a>
+          </p>
+          <el-upload :limit="1" :headers="ATOKEN" ref="upload1" :action="integralUpload" :on-success="handlePictureCardPreview" :before-upload="beforeFilesUpload">
+            <p>
+              3、上传填写好的员工信息表
+              <el-button type="primary" plain>选择文件</el-button>
+            </p>
+          </el-upload>
+          <p>4、选择文件后点击下方【上传】按钮</p>
+        </div>
+        <div class="align-center" style="margin-bottom:20px; float:left; margin-left:10px;"></div>
+        <div class="align-center" style="margin-bottom:20px; float:left; margin-left:10px;"><!-- <el-button type="primary" @click="downloadSheet">Excel导出</el-button> --></div>
+        <div style="clear:both;"></div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="close_import">取 消</el-button>
+        <el-button type="primary" @click="uploadFile(1)" :loading="update_btn" plain>上传</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 导出员工 -->
+    <el-dialog title="导出/修改员工信息" width="600px" :visible.sync="exportEmploye" :close-on-click-modal="false" :before-close="close_export">
+      <div style="float: left;width:49%">
+        <div style="margin-left: 20%;">
+          <p>1.导出员工信息</p>
+          <el-button type="primary" plain @click="downloadSheet">导出</el-button>
+        </div>
+      </div>
+      <div style="border-left: 1px solid #D8D8D8;width: 1px;height: 255px;float: left;"></div>
+
+      <div style="float: left;width:49%;">
+        <div style="margin-left: 20%;">
+          <p>2.修改员工信息</p>
+          <p>导出员工信息表进行修改</p>
+          <p>上传修改好的员工信息表</p>
+
+          <el-upload :limit="1" :headers="ATOKEN" ref="upload2" :action="integralUpload" :on-success="handlePictureCardPreview" :before-upload="beforeFilesUpload">
+            <p><el-button type="primary" plain>选择文件</el-button></p>
+          </el-upload>
+          <p>先选择文件后再完成上传</p>
+          <el-button type="primary" @click="uploadFile(2)" :loading="update_btn" plain>上传</el-button>
+        </div>
+      </div>
+
+      <div class="align-center" style="margin-bottom:20px; float:left; margin-left:10px;"></div>
+      <div class="align-center" style="margin-bottom:20px; float:left; margin-left:10px;"></div>
+      <div style="clear:both;"></div>
+    </el-dialog>
+
+    <!-- 添加员工 -->
+    <el-dialog :close-on-click-modal="false" ref="EditInformation_list" :title="title" width="500px" :before-close="detailFormreturn" :visible.sync="dialogTableVisible">
+      <div>
+        <EmploeAdd :managerId="managerId" :dialogTableVisibleNone="dialogTableVisibleNone" ref="EditInformation" @closeDetai="detailFormreturn" @refresh="refresh"></EmploeAdd>
+      </div>
+    </el-dialog>
+
+    <!-- 新增部门 -->
+    <el-dialog title="添加部门" width="500px" :visible.sync="dialogCreateVisible" :close-on-click-modal="false" @close="onDismissCreateDialog">
+      <el-form ref="newDeptForm" :model="newDeptForm" :rules="rules" @submit.native.prevent style="padding-right:20px;">
+        <el-form-item label="部门名称" prop="name" :label-width="formLabelWidth">
+          <el-input v-model="newDeptForm.name" placeholder="请输入部门名称" maxlength="15" show-word-limit auto-complete="off"></el-input>
+        </el-form-item>
+        <el-form-item label="上级部门" :label-width="formLabelWidth">
+          <el-input auto-complete="off" v-model="name" placeholder="请选择上级部门 (选填)"></el-input>
+          <div @click="show_dept_selector = true" style=" position: absolute; top: 0; right: 0; left: 0; bottom: 0; z-index: 9;"></div>
+          <EmployeeSelector
+            :employee_not_select="employee_not_select"
+            :can_select_employee="false"
+            :can_select_dept="true"
+            :dept_multi="false"
+            :dept_children="false"
+            :selected="dept_selected"
+            :visible.sync="show_dept_selector"
+            title="选择部门"
+            @confirm="move_confirm"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="colseCreateForm">取 消</el-button>
+        <el-button type="primary" :loading="add_dept_loading" @click="submitCreateForm('newDeptForm')">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 编辑部门 -->
+    <el-dialog title="编辑部门" width="500px" :visible.sync="dialogEditVisible" :close-on-click-modal="false" @close="onDismissEditDialog">
+      <el-form v-loading="get_dept_loading" ref="deptForm" :model="deptForm" :rules="rules" @submit.native.prevent style="padding-right:20px;">
+        <el-form-item label="部门名称" prop="name" :label-width="formLabelWidth">
+          <el-input v-model="deptForm.name" auto-complete="off" placeholder="请输入部门名称" maxlength="15" show-word-limit></el-input>
+        </el-form-item>
+        <el-form-item label="上级部门" :label-width="formLabelWidth">
+          <el-input auto-complete="off" v-model="name" placeholder="请选择上级部门"></el-input>
+          <div @click="show_dept_selector = true" style=" position: absolute; top: 0; right: 0; left: 0; bottom: 0; z-index: 9;"></div>
+          <EmployeeSelector
+            :employee_not_select="employee_not_select"
+            :can_select_employee="false"
+            :can_select_dept="true"
+            :dept_children="false"
+            :dept_multi="false"
+            :selected="dept_selected"
+            :visible.sync="show_dept_selector"
+            title="选择部门"
+            @confirm="move_confirm"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="danger" @click="deleteD" plain>删除部门</el-button>
+        <el-button @click="delteManger">取 消</el-button>
+        <el-button type="primary" :loading="add_dept_loading" @click="submitEditForm('deptForm')">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="导入结果" :visible.sync="error_list_show" :append-to-body="true" @close="error_list_SX" width="700px" :close-on-click-modal="false">
+      <el-table :data="error_list">
+        <el-table-column prop="line" label="行数">
+          <template slot-scope="scope">
+            <div>第{{ scope.row.line }}行</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="errors" label="备注信息"></el-table-column>
+      </el-table>
+    </el-dialog>
+
+    <!-- 设置主管 -->
+    <EmployeeSelector
+      :multi="false"
+      :deptId="filter.dept_id"
+      :isChecKedAll="false"
+      :child="false"
+      :is_filtration_creator="false"
+      title="设置管理员"
+      :visible.sync="show_employee_selector"
+      @confirm="move_employee_confirm"
+    />
+
+    <!-- 选择岗位 -->
+    <el-dialog title="选择岗位"  :visible.sync="isShowRule" class="fromNormGain_sty" top="12vh" width="1000px" append-to-body>
+      <div class="flex-box">
+        <div class="flex-2 kh-left">
+          <ul class="scroll-bar ul" v-if="tabs.length > 0" style="height: 440px;overflow: auto;">
+            <li class="flex-box-ce li" v-for="(item, index) in tabs" :key="index" :class="[tabItem.id == item.id ? 'isActiveLi' : '']" @click="activeLi(item, index)">
+              <div class="index-name font-flex-word"><i class="el-icon-s-help" v-if="item.id==-1"></i> {{ item.name }}</div>
+            </li>
+          </ul>
+        </div>
+        <div class="flex-4">
+          <el-input style="width: 200px;" size="small" maxlength="100" show-word-limit v-model="postName" clearable placeholder="搜索"/>
+          <div class="table-box">
+            <el-table :data="list" stripe fit v-loading="table_loading"  style="cursor: pointer;" @row-click="clickdatail">
+              <el-table-column width="40" fixed>
+                <template slot-scope="scope">
+                    <div style="width: 40px;text-align: center;position: relative;">
+                      <el-checkbox v-model="scope.row.checked"></el-checkbox>
+                      <div style="width: 40px;height: 40px;position: absolute;top: -10px;z-index: 999;"></div>
+                    </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="岗位名称" prop="name"></el-table-column>
+              <el-table-column label="岗位职责" prop="desc_1" show-overflow-tooltip></el-table-column>
+              <template slot="empty">
+                <NoData></NoData>
+              </template>
+            </el-table>
+            <center>
+              <el-pagination
+                small
+                :page-size="page_size"
+                :current-page="page"
+                layout="prev, pager, next"
+                @size-change="handleSizeChange"
+                @current-change="handleCurrentChange"
+                :total="total">
+              </el-pagination>
+            </center>
+          </div>
+        </div>
+        <div>
+          <div class="mainEl" style="border-left: 1px solid #f1f1f1;width: 240px;padding: 10px;">
+            <div class="mainHead flex-box flex-d-center" v-if="selectItem.length > 0">
+              <span>已选择{{ selectItem.length }}个</span>
+              <span @click="Empty">清空</span>
+            </div>
+            <div class="mainTag">
+              <el-tag v-for="(item, index) in selectItem" :key="index" closable :disable-transitions="true" @close="TagClose(item, index)">{{ item.name }}</el-tag>
+            </div>
+          </div>
+        </div>
+      </div>
+      <span slot="footer">
+        <el-button @click="isShowRule=false">取 消</el-button>
+        <el-button type="primary" @click="ruleConfirm">确认</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 离职人员列表 -->
+    <el-dialog title="离职人员列表"  :visible.sync="isShowDelete"  top="5vh" width="800px">
+      <el-table :data="deleteUserList"  style="max-height: 600px;overflow-y: scroll;">
+        <el-table-column prop="name" label="姓名">
+          <template slot-scope="scope">
+            <div class="flex-box-ce">
+              <userImage  :id="scope.row.id" :user_name="scope.row.name" :img_url="scope.row.img_url" fontSize="16" width="50px" height="50px"></userImage>
+              <span style="margin-left: 10px; line-height: 50px; display: inline-block;">{{ scope.row.name }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="dept_list" label="部门">
+          <template slot-scope="scope">
+            <template v-if="scope.row.employee_detail.dept_list.length > 0">
+              <span v-for="(item, index) in scope.row.employee_detail.dept_list" :key="index">
+                {{ item.dept_name }}
+                <span v-if="scope.row.employee_detail.dept_list.length - index > 1">,</span>
+              </span>
+            </template>
+            <span v-else>--</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="tel" label="电话"></el-table-column>
+        <el-table-column prop="post" label="岗位">
+          <template slot-scope="scope">
+                <span>{{scope.row.post||'--'}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="80">
+          <template slot-scope="scope">
+            <el-button size="small" plain type="danger" @click="delteOpert(scope.row.id)">删除</el-button>
+          </template>
+        </el-table-column>
+        <template slot="empty"><NoData></NoData></template>
+      </el-table>
+      <center>
+        <el-pagination
+          :current-page.sync="delete_page"
+          :page-sizes="[10]"
+          layout="total,prev,pager,next"
+          :total="delete_total"
+          @size-change="handleSizeChange2"
+          @current-change="handleCurrentChange2"
+          :page-size="delete_page_size">
+        </el-pagination>
+      </center>
+      <span slot="footer">
+        <el-button @click="isShowDelete=false">关 闭</el-button>
+      </span>
+    </el-dialog>
+
+  </div>
+</template>
+<script>
+import EmploeAdd from '@/components/organization/EmploeAdd';
+import EmployeeSelector from '@/components/EmployeeSelector';
+import qs from 'qs';
+import {_debounce} from '@/utils/auth';
+export default {
+  components: {
+    EmploeAdd,
+    EmployeeSelector
+  },
+  data() {
+    return {
+      selectedByDefault: {},
+      integralUpload: this.$serverdomain + '/api/pro/upload/excel',
+      dialogTableVisibleNone: false, //是否显示 员工标识
+
+      get_dept_loading: false,
+      titleManger: false,
+      nowEmploye: 0,
+      ATOKEN: { 'A-TOKEN': this.$getToken() },
+      file: '', // 文件地址
+
+      title: '添加员工',
+      managerId: 0, // 员工id
+      firstCompany: {},
+      supermanger: {}, //管理者列表
+      company: {},
+      pid: '',
+      name: '',
+      manager: '',
+
+
+      treedata: [], // 菜单栏数组
+      exportEmploye: false, // 导出员工信息
+
+      rules: {
+        name: [{ required: true, message: '请输入名称'}, { min: 1, max: 15, message: '长度在 1 到 15 个字符'}]
+      },
+
+      defaultExpand: false,
+
+      show3: false,
+      textTip: '选择部门进行筛选',
+      loading: false,
+
+      dialogTableVisible: false,
+      filter: {
+        dept_id: 0,
+        keywords: ''
+      },
+      totalCount: 0,
+      pageLimit: 10,
+      currentPage: 1, // 不缓存页面,因为el-pagination此处有一个bug,规定页面从0开始
+      tableData: null,
+      multipleSelection: [],
+      dialogExcelVisible: false,
+      deptTree: [],
+
+
+      defaultProps: {
+        children: 'children',
+        label: 'label'
+      },
+      formLabelWidth: '100px',
+
+      dialogCreateVisible: false, // 新增部门
+      dialogEditVisible: false, // 编辑部门
+      // 新增部门
+      newDeptForm: {
+        name: '',
+        pid: ''
+      },
+      // 编辑部门
+      deptForm: {
+        name: '',
+        pid: ''
+      },
+      add_dept_loading: false,
+
+      error_list_show: false,
+      error_list: [],
+      dept_show: false,
+      update_btn: false,
+      selected: [],
+
+      employee_not_select: [],
+      show_dept_selector: false,
+      show_employee_selector: false,
+      dept_selected: { employee: [], dept: [] },
+      manager_selected: { employee: [], dept: [] },
+      tips_show: false,
+      set_btn: false,
+
+      options: [
+        // {
+        //   value: '-1',
+        //   label: '全部'
+        // },
+        {
+          value: '1',
+          label: '在职'
+        },
+        {
+          value: '0',
+          label: '待加入'
+        },
+        // {
+        //   value: '2',
+        //   label: '离职'
+        // },
+        {
+          value: '3',
+          label: '再次邀请'
+        }
+      ],
+      status: '1',
+      account_id:false,
+
+      total: 0,
+      page: 1,
+      page_size: 7,
+      tabItem:{name:'销售'},
+      tabs:[{name:'全部分类',id:-1}],
+      table_loading:false,
+      list:[],
+      isShowRule:false,
+      selectItem:[],
+      selectId:'',
+      postName:'',
+
+      deleteLoading:false,
+      deleteUserList:[],
+      isShowDelete:false,
+      delete_total: 0,
+      delete_page: 1,
+      delete_page_size: 10,
+    };
+  },
+  watch: {
+    'filter.keywords': _debounce(function(val) {
+       this.onFilterChanged();
+    }),
+
+    isShowDelete(val){
+      if(val){
+        this.delete_page=1;
+        this.getDeleteUserList();
+      }
+    },
+    postName: {
+      deep: true,
+      handler: _debounce(function(val) {
+        this.page=1;
+        this.page_size=7;
+        this.getTableData();
+      })
+    },
+    status(val) {
+      this.currentPage = 1;
+      this.loadEmployeeList();
+    },
+    account_id(val){
+       if(val){
+         this.status='1';
+         this.currentPage = 1;
+         this.loadEmployeeList();
+       }else{
+          this.loadEmployeeList();
+       }
+    }
+  },
+  methods: {
+    // 获取员工列表
+    getDeleteUserList() {
+      this.deleteLoading = true;
+      let params = {
+        page: this.delete_page,
+        page_size: this.delete_page_size,
+        status: 2
+      };
+      this.$axiosUser('get', '/api/pro/employee/index', params, 'v2').then((res)=> {
+        this.delete_total = res.data.data.total;
+        this.deleteUserList = res.data.data.list;
+      }).finally(() => {
+        this.deleteLoading = false;
+      });
+    },
+    // 删除员工
+    delteOpert(id) {
+      this.$confirm('删除员工后,此员工的相关数据将被清空,请确认系统中的数据和操作执行人转交完毕后再进行删除操作?', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+          this.$axiosUser('post', '' + '/api/pro/employee/delete?id=' + id).then(res => {
+              this.$message({message: '删除成功',type: 'success'});
+              this.getDeleteUserList();
+          });
+      }).catch(() => {});
+    },
+    openPost(item){
+        this.selectItem=JSON.parse(JSON.stringify(item.post_info));
+        this.selectId=item.id;
+        this.postName="";
+        this.isShowRule=true;
+        this.getList();
+    },
+    TagClose(item, index) {
+      //清除右侧tag
+      this.selectItem.splice(index, 1);
+    },
+    Empty() {
+      this.selectItem = [];
+      this.list.forEach(item=>{
+        item.checked=false
+      })
+    },
+    clickdatail(item){
+      item.checked=!item.checked;
+      if(item.checked){
+        this.selectItem.push(item);
+      }else{
+        this.selectItem.forEach((e, index) => {
+          if (e.id == item.id) {
+            this.selectItem.splice(index, 1);
+          }
+        });
+      }
+    },
+    activeLi(item, index) {
+      this.tabItem = JSON.parse(JSON.stringify(item));
+      this.page=1;
+      this.page_size=7;
+      this.getTableData();
+    },
+    getList() {
+      this.tabs=[{name:'全部分类',id:-1}];
+      this.$axiosUser('get', '/api/pro/post/post_cate_list').then(res => {
+          this.tabs.push(...res.data.data)
+          this.tabItem = JSON.parse(JSON.stringify(this.tabs[0]));
+          this.getTableData();
+      })
+    },
+    getTableData() {
+      this.table_loading=true;
+      let data={
+        page:this.page,
+        page_size:this.page_size,
+        cate_id:this.tabItem.id,
+        name:this.postName
+      }
+      this.$axiosUser('get', '/api/pro/post/cate_post_list',data).then(res => {
+          let list=res.data.data.list;
+          let selectItem=this.selectItem;
+          list.forEach(item=>{
+            item.checked=false
+            for (var i in selectItem) {
+              if (selectItem[i].id == item.id) {
+                item.checked = true;
+              }
+            }
+          })
+          this.list=list;
+          this.total=res.data.data.total
+      }).finally(()=>{
+           this.table_loading=false;
+      });
+    },
+    ruleConfirm(){
+      // if(this.selectItem.length>3){
+      //   this.$message.warning('关联岗位数量不能超过3个');
+      //   return false
+      // }
+
+      let post_ids=this.selectItem.map(e=>{
+        return e.id
+      })
+      let data={
+        employee_id:this.selectId,
+        post_ids:JSON.stringify(post_ids)
+      }
+      this.$axiosUser('post', '/api/pro/post/bind_post',data).then(res => {
+          this.$message.success('已编辑');
+          this.loadEmployeeList();
+          this.isShowRule=false;
+      })
+    },
+    handleSizeChange2(val) {
+      this.delete_page=1;
+      this.delete_page_size = val;
+      this.getDeleteUserList()
+    },
+    // 页面跳转
+    handleCurrentChange2(val) {
+      this.delete_page = val;
+      this.getDeleteUserList()
+    },
+    // 页面变更
+    handleCurrentChange(val) {
+      this.page = val;
+      this.getTableData();
+    },
+    // 页面跳转
+    handleSizeChange(val) {
+      this.page_size = val;
+      this.getTableData();
+    },
+    //添加管理者
+    move_employee_confirm(data) {
+      var that = this;
+      that.$axiosUser('POST', '/api/pro/department/manage/add', { employee_id: data.employee[0].id, dept_id: that.filter.dept_id }).then(res => {
+        if (res.data.code == 1) {
+          that.$message.success('添加成功');
+          that.loadEmployeeList();
+        }
+      });
+    },
+
+    //删除管理者
+    deleteEmployee(tag) {
+      var that = this;
+      this.$confirm('是否删除该管理者?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          var data = {
+            employee_id: tag.id,
+            dept_id: that.filter.dept_id
+          };
+          that.$axiosUser('DELETE', '/api/pro/department/manage', data).then(res => {
+            if (res.data.code == 1) {
+              that.$message.success('删除成功');
+              that.loadEmployeeList();
+            }
+          });
+        })
+        .catch(() => {});
+    },
+
+    //再次邀请
+    againInvite(item) {
+      let site_id = this.$getCache('site_info').id;
+      this.$axiosUser('post', '/api/pro/invite/again', { site_id: site_id, employee_id: item.id }).then(res => {
+        if (res.data.code == 1) {
+          this.$message({
+            message: res.data.msg,
+            type: 'success'
+          });
+          this.loadEmployeeList();
+        }
+      });
+    },
+    //添加部门点击
+    addTheDepartment() {
+      this.name = '';
+      this.manager = '';
+      this.manager_selected = { employee: [], dept: [] };
+      if (this.selectedByDefault.id) {
+        //默认上级未目前所选择的部门
+        this.name = this.selectedByDefault.name;
+        this.newDeptForm.pid = this.selectedByDefault.id;
+        this.dept_selected.dept = [{ dept_name: this.selectedByDefault.name, dept_id: this.selectedByDefault.id }];
+      } else {
+        this.dept_selected = { employee: [], dept: [] };
+      }
+      this.dialogCreateVisible = true;
+    },
+    tips_close() {
+      this.$setCache('employee_table_tips', 'true');
+      this.tips_show = false;
+    },
+
+    // 递归判断列表,把最后的children设为undefined
+    getTreeData(data) {
+      for (var i = 0; i < data.length; i++) {
+        if (data[i].children.length < 1) {
+          data[i].children = undefined;
+        } else {
+          this.getTreeData(data[i].children);
+        }
+      }
+      return data;
+    },
+    move_confirm(data) {
+      if (data.dept !== null && data.dept.length != 0) {
+        this.name = data.dept[0].dept_name;
+        this.newDeptForm.pid = data.dept[0].dept_id;
+        this.deptForm.pid = data.dept[0].dept_id;
+        this.dept_selected.dept = [{ dept_name: data.dept[0].dept_name, dept_id: data.dept[0].dept_id }];
+      } else {
+        this.name = '';
+        this.deptForm.pid = 0;
+        this.newDeptForm.pid=0;
+        this.dept_selected = { employee: [], dept: [] };
+      }
+    },
+
+    // 关闭上传窗口
+    remove(file, fileList) {},
+
+    // 新成员列表
+    new_employee() {
+      this.$router.push('/new_employee');
+    },
+    // 关闭导入
+    close_import() {
+      this.dialogExcelVisible = false;
+      this.$refs.upload1.clearFiles();
+      this.file = '';
+    },
+    // 关闭导出
+    close_export() {
+      this.exportEmploye = false;
+      this.$refs.upload2.clearFiles();
+      this.file = '';
+    },
+    //关闭错误信息时的回调
+    error_list_SX() {
+      this.set_btn = false;
+      this.loadBaseData();
+      this.loadEmployeeList();
+    },
+    //  导入 新增
+    uploadFile: function(type) {
+      let params = {};
+      params.type = type;
+      params.file = this.file;
+      this.update_btn = true;
+      this.$axiosUser('post', '/api/pro/employee/import', params).then(res => {
+          if (res.data.code == 1) {
+            if (type == 1) {
+              this.close_import();
+            } else if (type == 2) {
+              this.close_export();
+            }
+            this.$message.success({
+              type: '上传成功',
+              message: res.data.msg
+            });
+            if (res.data.data.length != 0 && res.data.data.length != undefined) {
+              this.error_list_show = true;
+              this.error_list = res.data.data;
+            } else {
+              this.set_btn = false;
+              this.loadBaseData();
+              this.loadEmployeeList();
+            }
+          }
+      }).finally(() => {
+          setTimeout(() => {
+            this.update_btn = false;
+          }, 3000);
+      });
+    },
+    handlePictureCardPreview(response) {
+      if (response.code == 1) {
+        this.file = response.data;
+      }
+    },
+    beforeFilesUpload(file) {
+      const $ext_list = ['xlsx', 'xls', 'XLSX', 'XLS'];
+      let len = file.name.split('.').length - 1;
+      const $ext_name = file.name.split('.')[len];
+      if ($ext_list.indexOf($ext_name) != -1) {
+      } else {
+        this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls)');
+        return false;
+      }
+    },
+    // 获取回传的上级部门id
+    check(val) {
+      if (this.firstCompany.name == val[0].label) {
+        this.newDeptForm.pid = 0;
+        this.deptForm.pid = 0;
+      } else {
+        this.newDeptForm.pid = val[0].id;
+        this.deptForm.pid = val[0].id;
+      }
+      this.name = val[0].label; // 部门名称
+    },
+
+    refresh() {
+      this.loadEmployeeList();
+    },
+    // 删除部门
+    deleteD() {
+      this.$confirm('确定要删除此部门吗?', '删除部门', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.deleteDepart();
+      });
+    },
+
+    // 折叠菜单栏
+    setCheckedKeys() {
+      this.defaultExpand = false;
+      for (var i = 0; i < this.$refs.menum.store._getAllNodes().length; i++) {
+        this.$refs.menum.store._getAllNodes()[i].expanded = false;
+      }
+    },
+    handleNodeClick(data) {
+
+      if (data.value == 0) {
+        this.company = this.firstCompany;
+        this.selectedByDefault = {};
+        this.filter.dept_id = 0;
+        this.set_btn = false;
+        this.setCheckedKeys();
+        this.$refs.menum.setCheckedKeys([]);
+      } else {
+        this.selectedByDefault = data;
+        this.set_btn = true;
+        this.company = data;
+        this.filter.dept_id = data.id;
+      }
+      this.status = '1';
+      this.onFilterChanged();
+      this.showpull = false;
+    },
+    // 节点展开
+    treeOpen() {
+      let that = this;
+      setTimeout(function() {
+        that.getHeight();
+      }, 500);
+    },
+    onDismissCreateDialog: function() {
+      //更新树形图
+      var form = this.$refs['newDeptForm'];
+      form.clearValidate();
+      this.newDeptForm = {
+        name: '',
+        pid: []
+      };
+    },
+    // 关闭编辑弹框
+    onDismissEditDialog: function() {
+      var form = this.$refs['deptForm'];
+      form.clearValidate();
+      this.deptForm = {
+        id: '',
+        name: '',
+        pid: []
+      };
+    },
+    // 删除部门
+    deleteDepart() {
+      let self = this;
+      let id = this.company.id;
+      this.$axiosUser('post', '/api/pro/department/delete', { id: id }, 'v2').then(res => {
+        if (res.data.code == 1) {
+          localStorage.removeItem('dept_tree');
+          this.$message({
+            message: '删除成功',
+            type: 'success'
+          });
+          self.dialogEditVisible = false;
+          self.wipe_data();
+          self.setCheckedKeys();
+          self.loadReftList();
+          self.loadBaseData();
+          this.set_btn = false;
+          this.company.name = this.firstCompany.name;
+          this.filter.dept_id = 0;
+          this.selectedByDefault = {};
+          self.loadEmployeeList();
+        } else {
+          self.$message({
+            message: res.data.msg,
+            type: 'error'
+          });
+        }
+      });
+    },
+    // 关闭编辑部门的弹框
+    delteManger() {
+      let self = this;
+      self.dialogEditVisible = false;
+      self.name = '';
+      self.manager = '';
+    },
+    // 编辑部门
+    submitEditForm: function(formName) {
+      var form = this.$refs[formName];
+      var self = this;
+      form.validate(valid => {
+        if (valid) {
+          if (form) {
+            self.sendEditRequest(form);
+          }
+        } else {
+          return false;
+        }
+      });
+    },
+    // 编辑部门方法
+    sendEditRequest: function() {
+      var self = this;
+      self.add_dept_loading = true;
+      let id = this.company.id;
+      if (this.deptForm.pid == '') {
+        this.deptForm.pid = 0;
+      }
+      this.deptForm.id = id;
+      this.$axiosUser('post', '/api/pro/department/update', this.deptForm, 'v2')
+        .then(function(res) {
+          if (res.data.code == 1) {
+            self.filter.dept_id = 0;
+            localStorage.removeItem('dept_tree');
+            self.loadEmployeeList();
+            self.loadBaseData();
+            self.dialogEditVisible = false;
+            self.$message({
+              message: '编辑成功',
+              type: 'success'
+            });
+            self.wipe_data();
+            self.handleNodeClick({ value: 0 });
+          } else {
+            self.$message({
+              type: 'error',
+              message: res.data.msg
+            });
+          }
+        })
+        .finally(() => {
+          self.add_dept_loading = false;
+        });
+    },
+
+    // 新增部门
+    sendCreateRequest: function() {
+      this.add_dept_loading = true;
+      if (this.newDeptForm.pid == '') {
+        this.newDeptForm.pid = 0;
+      }
+      this.$axiosUser('post', '/api/pro/department/create', this.newDeptForm, 'v2').then((res)=> {
+            localStorage.removeItem('dept_tree');
+            this.$message({
+              message: '新建部门成功',
+              type: 'success'
+            });
+            this.filter.dept_id = 0;
+            this.company.name = this.firstCompany.name;
+            this.currentPage = 1;
+            this.set_btn = false;
+            this.setCheckedKeys();
+            this.loadReftList();
+            this.loadBaseData();
+            this.dialogCreateVisible = false;
+            this.loadEmployeeList();
+            this.wipe_data();
+            this.$refs.newDeptForm.resetFields();
+        }).finally(() => {
+          this.add_dept_loading = false;
+        });
+    },
+
+    // 提交表单
+    submitCreateForm: function(formName) {
+      let form = this.$refs[formName];
+      var self = this;
+      form.validate(valid => {
+        if (valid) {
+          self.sendCreateRequest();
+        } else {
+          return false;
+        }
+      });
+    },
+    colseCreateForm() {
+      this.dialogCreateVisible = false;
+      this.wipe_data();
+    },
+    //获取部门列表
+    loadReftList: function() {
+      this.loading = true;
+      var self = this;
+    },
+
+    managerType(row, column, cellValue) {
+      return row.is_admin == 1 ? '超级管理员' : cellValue;
+    },
+    handleCheck(target, param) {
+      this.textTip = '';
+      const textTip = [];
+      this.$refs.tree.getCheckedNodes().forEach(item => {
+        textTip.push(item.label);
+      });
+      this.textTip = textTip.join('、');
+      if (param.checkedKeys.length == 0) {
+        this.textTip = '选择部门进行筛选';
+        // this.show3 = false;
+      }
+      this.filter.dept_id = param.checkedKeys;
+      this.onFilterChanged();
+      this.show3 = false;
+    },
+
+
+    filterHandler(value) {
+      if (typeof value.dept !== 'undefined' && value.dept.length == 0) {
+        this.filter.dept_id = 0;
+      }
+      if (typeof value.dept !== 'undefined' && value.dept.length > 0) {
+        this.filter.dept_id = value.dept;
+      }
+
+      this.onFilterChanged();
+    },
+
+    EmployeeAdd() {
+      this.dialogTableVisible = true;
+      this.dialogTableVisibleNone = false;
+      this.title = '添加员工';
+      const self = this;
+      self.managerId = 0;
+      this.$nextTick(() => {
+        self.$refs['EditInformation'].editMode = false;
+        self.$refs['EditInformation'].clearForm();
+      });
+    },
+    handleSizeChange(val) {
+      this.pageLimit = val;
+      this.onFilterChanged();
+    },
+
+    onFilterChanged: function() {
+      this.currentPage = 1;
+      this.loadEmployeeList();
+    },
+    checkSelectable(row) {
+      return;
+    },
+    handleSelectionChange: function(val) {
+      this.multipleSelection = val;
+    },
+    isSelectable: function(row, index) {
+      return row.status != '1';
+    },
+    toSexName: function(row, column, cellValue) {
+      return cellValue == '1' ? '女' : '男';
+    },
+    dateFormatter: function(row, column, cellValue) {
+      return this.$moment(cellValue * 1000).format('YYYY年MM月DD日');
+    },
+    // 获取基本数据
+    loadBaseData: function() {
+      var self = this;
+      this.dept_show = true;
+      this.$axiosUser('get', '/api/pro/department/tree', '', 'v2')
+        .then(function(response) {
+          if (response.status == 200) {
+            var jsonData = response.data;
+            try {
+              self.deptTree = self.getTreeData(response.data.data.list);
+              self.company = response.data.data.company;
+              self.firstCompany = response.data.data.company;
+              self.treedata = self.deptTree;
+            } catch (err) {}
+          }
+        })
+        .finally(() => {
+          this.dept_show = false;
+        });
+    },
+    // 获取员工列表
+    loadEmployeeList: function() {
+      var self = this;
+      self.loading = true;
+      let params = {
+        page: this.currentPage,
+        page_size: this.pageLimit,
+        dept_id: this.filter.dept_id,
+        keywords: this.filter.keywords,
+        status: this.status,
+      };
+      if (this.status == 1) {
+        params.account_id = this.account_id? 1:0;
+      }
+      this.$axiosUser('get', '/api/pro/employee/index', params, 'v2')
+        .then(function(res) {
+          if (res.data.code == 1) {
+            self.totalCount = res.data.data.total;
+            self.nowEmploye = res.data.data.total;
+            self.tableData = res.data.data.list;
+            self.supermanger = res.data.data.managers;
+            if (self.supermanger != undefined && self.supermanger.length > 0 && self.supermanger[0].dept_list == '[]') {
+              self.titleManger = true;
+            } else {
+              self.titleManger = false;
+            }
+          }
+        })
+        .finally(() => {
+          self.loading = false;
+        });
+    },
+    changePage: function(current) {
+      this.loadEmployeeList();
+    },
+    delEmployee: function(employeeId) {
+      let self = this;
+      self
+        .$confirm('确定要删除此员工吗?', '删除员工', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        .then(() => {
+          self
+            .$axiosUser('POST', '/api/pro/employee/delete', { id: employeeId })
+            .then(res => {
+              if (res.data.code == 1) {
+                self.$message.success(res.data.msg);
+                self.loadEmployeeList();
+              }
+            })
+            .catch(e => {
+              self.$message.error(e.data.msg);
+            })
+            .finally(() => {
+              self.role_loading = false;
+            });
+        });
+    },
+    confirmMultipleDelete: function() {
+      if (this.multipleSelection.length < 1) {
+        return false;
+      }
+      this.$confirm('确定要删除选中的员工吗?', '批量删除员工', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          const targetArr = [];
+          for (const index in this.multipleSelection) {
+            targetArr.push(this.multipleSelection[index].id);
+          }
+          this.$axiosUser('post', '/api/pro/employee/delete-many', { id: targetArr }).then(res => {
+            if (res.data.code == 1) {
+              this.$message({
+                type: 'success',
+                message: '批量删除成功'
+              });
+              this.loadEmployeeList();
+              this.multipleSelection = [];
+            } else {
+              this.$message({
+                type: 'error',
+                message: res.data.msg
+              });
+            }
+          });
+        })
+        .catch(() => {});
+    },
+
+    detailFormreturn: function() {
+      this.dialogTableVisible = false;
+      // this.$refs['EditInformation'].discardBack();
+    },
+    // 获取部门详情
+    openEmploe: function() {
+      this.dialogEditVisible = true;
+      this.get_dept_loading = true;
+      let id = this.company.id;
+      this.$axiosUser('get', '/api/pro/department/info', { id: id }, 'v2')
+        .then(res => {
+          if (res.data.code == 1) {
+            let data = res.data.data;
+            this.deptForm.name = data.name;
+            if (data.super_department != null) {
+              this.deptForm.pid = data.super_department.id; // 上级部门id
+              this.name = data.super_department.name;
+              this.dept_selected.dept = [{ name: data.super_department.name, id: data.super_department.id }];
+            } else {
+              if (data.pid == 0) {
+                this.deptForm.pid = 0;
+                this.name = this.firstCompany.name;
+              }
+            }
+            if (data.manager.length > 0) {
+              let newmangerId = [];
+              let newmanger = [];
+              for (let i = 0; i < data.manager.length; i++) {
+                newmangerId.push(data.manager[i].id);
+                newmanger.push(data.manager[i].name);
+              }
+              this.manager = newmanger.toString();
+              let managerSelecteds = [];
+              for (let i in data.manager) {
+                managerSelecteds.push({
+                  name: data.manager[i].name,
+                  id: data.manager[i].id,
+                  img_url: data.manager[i].img_url
+                });
+              }
+              this.manager_selected.employee = managerSelecteds;
+            } else {
+              this.manager = '';
+              this.manager_selected.employee = data.manager;
+            }
+            if (data.super_department) {
+              this.dept_selected.dept = [{ dept_name: data.super_department.name, dept_id: data.super_department.id }];
+            }
+          }
+        })
+        .finally(() => {
+          this.get_dept_loading = false;
+        });
+    },
+
+    // 编辑员工
+    toDetailPage: function(item, column, event) {
+      this.managerId = item.id;
+      this.title = '编辑员工';
+      this.$nextTick(() => {
+        this.$refs['EditInformation'].clearForm();
+        this.$refs['EditInformation'].loadEmployeeDetail(item.id);
+        this.$refs['EditInformation'].editMode = true;
+      });
+      // 编辑打开添加员工窗口
+      this.dialogTableVisible = true;
+      this.dialogTableVisibleNone = true;
+    },
+    saveStorage: function() {
+      const storage = {
+        filter: this.filter
+      };
+      this.$emit('set-storage', storage);
+    },
+    // 导出员工信息
+    downloadSheet: function() {
+      let params = {};
+      if (this.filter.dept_id != '') {
+        params.dept_id = this.filter.dept_id;
+      } else if (this.filter.keywords != '') {
+        params.keywords = this.filter.keywords;
+      }
+      this.$axiosUser('get', '/api/pro/employee/export', params).then(res => {
+        if (res) {
+          if (res.data.code == 1) {
+            let path = res.data.data.path;
+            window.open(this.$serverdomain + '/api/pro/employee/download?path=' + path + '&type=1');
+          }
+        }
+      });
+    },
+    getHeight() {},
+    wipe_data() {
+      this.pid = '';
+      this.name = '';
+      this.manager = '';
+    }
+  },
+  mounted() {
+    let that = this;
+    setTimeout(function() {
+      that.getHeight();
+    }, 500);
+    if (this.$getCache('employee_table_tips')) {
+      that.tips_show = false;
+    } else {
+      that.tips_show = true;
+    }
+  },
+  created() {
+    if (this.$route.query.is) {
+      this.$router.push({ path: '/new_employee' });
+    }
+    this.loadBaseData();
+    this.loadEmployeeList();
+    this.$nextTick(() => {
+      document.querySelector('#app').addEventListener('click', () => {
+        if (this.show3) {
+          this.show3 = false;
+        }
+      });
+    });
+  },
+  beforeDestroy(){
+     this.$store.dispatch('get_dept_tree',true).then((res) => {}) // 获取部门树型结构列表
+  }
+
+};
+</script>
+<style lang="scss" scoped>
+.selectVal /deep/ .el-input__inner {
+  border: none;
+}
+.selectVal /deep/ .el-select__caret {
+  color: #409EFF;
+}
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+.name {
+  text-overflow: ellipsis;
+  display: inline-block;
+  white-space: nowrap;
+  overflow: hidden;
+  width: 120px;
+}
+.administration {
+  color: #e6a23c;
+  border: 1px solid #e6a23c;
+  border-radius: 2px;
+  font-size: 12px;
+  margin-left: 4px;
+  padding: 2px;
+}
+
+.el-table th > .cell {
+  font-weight: normal;
+}
+.el-table .has-gutter th > .cell {
+  background-color: #fff;
+  font-size: 14px;
+  color: #909399;
+  font-weight: normal;
+}
+.title {
+  color: #303133;
+  font-size: 20px;
+}
+.el-button--text {
+  color: #606266;
+}
+.add {
+  color: #409EFF;
+  font-size: 14px;
+  cursor: pointer;
+}
+.el-pagination {
+  text-align: center;
+  margin-top: 15px;
+}
+.arrRotation {
+  transform: rotate(180deg);
+}
+.treeActice {
+  color: #606266;
+}
+
+.treeIcon {
+  display: none;
+}
+.custom-tree-node {
+  margin-left: -4px;
+}
+.custom-tree-node * {
+  vertical-align: middle;
+}
+.custom-tree-node:hover {
+  .treeIcon {
+    display: inline-block;
+    width: 55%;
+  }
+}
+.item:hover {
+  .blueTree {
+    display: block;
+  }
+  .iconTree {
+    display: none;
+  }
+}
+
+.blueTree {
+  display: none;
+}
+el-tooltip {
+  display: none;
+}
+.add {
+  color: #409EFF;
+  font-size: 14px;
+  cursor: pointer;
+}
+.all-box {
+  text-align: center;
+  background-color: #fff;
+  overflow: hidden;
+}
+.all-box .RuleLine {
+  display: table-cell;
+  width: 1px;
+  min-height: 600px;
+  background: #efefef;
+}
+.all-box .RuleLeft {
+  text-align: center;
+  padding: 20px 10px;
+  overflow: auto;
+  max-height: 700px;
+}
+.all-box .RuleLeft .company_name {
+  position: relative;
+  display: block;
+  font-family: 'Microsoft YaHei';
+  text-align: left;
+  padding: 15px 28px 17px;
+  cursor: pointer;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  border-bottom: 1px #f8f8f8 solid;
+}
+.all-box .RuleLeft .company_name img {
+  position: relative;
+  display: inline-block;
+  top: 2px;
+  width: 18px;
+  height: 18px;
+  margin-right: 4px;
+}
+.all-box .RuleLeft /deep/ .el-button {
+  margin-bottom: 16px !important;
+}
+.all-box .RuleRight {
+  position: relative;
+  text-align: left;
+  padding: 20px;
+  min-width: 600px;
+}
+.all-box .RuleRight .title span .sapn {
+  display: inline-block;
+  vertical-align: middle;
+  max-width: 600px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.all-box .RuleRight:after {
+  content: ' ';
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: 1px;
+  background: #ebeef5;
+}
+.all-box /deep/ .el-table__row {
+  cursor: pointer;
+}
+
+.all-box .department_box /deep/ .el-tree-node {
+  display: block;
+  text-align: center;
+}
+.all-box .department_box /deep/ .el-tree-node__content {
+  height: auto !important;
+}
+
+.all-box .department_box /deep/ .el-tree-node .el-icon-caret-right {
+  display: inline-block;
+  color: #909399;
+  font-size: 16px;
+  padding: 6px 8px;
+}
+.all-box .department_box /deep/ .el-tree-node .el-icon-caret-right.is-leaf {
+  color: transparent;
+  cursor: default;
+}
+.all-box .department_box /deep/ .el-tree-node .custom-tree-node img {
+  margin-top: 0px;
+  width: 20px;
+}
+.all-box .department_box /deep/ .el-tree-node .custom-tree-node span {
+}
+.all-box .department_box /deep/ .el-tree-node__content {
+  padding: 12px 0;
+  border-bottom: 1px #f8f8f8 solid;
+}
+.all-box .department_box /deep/ .el-tree-node__content:hover {
+  background: #ecf5ff;
+  border-radius: 4px;
+}
+.all-box .department_box /deep/ .is-focusable .is-current {
+  border-radius: 4px;
+}
+.all-box .department_box /deep/ .is-focusable .is-current .name {
+  color: #409EFF;
+  font-weight: normal;
+  transition: 0.35s ease-in-out;
+}
+.diy-tip {
+  margin-bottom: 10px;
+  border: 1px solid #67c23a;
+  padding: 20px 16px;
+  p {
+    color: #67c23a !important;
+    font-size: 14px;
+    margin: 0 !important;
+    padding: 4px 0;
+  }
+}
+  .kh-right {
+    padding: 0 10px;
+  }
+  .pagination {
+    position: relative;
+    left: -10px;
+  }
+  .liAction {
+    background-color: #ebeef5;
+    color: #409EFF;
+  }
+  .kh-title {
+    text-align: center;
+    font-weight: 700;
+    border-bottom: 1px solid #ebeef5;
+    padding-bottom: 10px;
+  }
+  .kh-left {
+    border-right: 1px solid #ebeef5;
+  }
+  .kh-left li {
+    padding: 10px;
+    cursor: pointer;
+    border-bottom: 1px solid #ebeef5;
+  }
+  .kh-left li:hover {
+    background-color: #ebeef5;
+  }
+  .kh-Box {
+    // height: 430px;
+  }
+.bmxx .el-dialog__body {
+  padding: 30px 100px 20px 10px !important;
+}
+.el-form-item__label {
+  font-weight: normal;
+}
+.li:hover {
+  background-color: #f5f7fa;
+}
+.li:hover .el-icon-more {
+  display: block;
+}
+.index-name {
+  width: 170px;
+}
+.ul {
+  max-height: calc(100vh - 230px);
+  overflow: auto;
+}
+.ul::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+.ul:hover::-webkit-scrollbar-thumb {
+  background-color: #ccc;
+}
+.ul::-webkit-scrollbar-track {
+  width: 6px;
+  background-color: #f1f1f1;
+  -webkit-border-radius: 2em;
+  -moz-border-radius: 2em;
+  border-radius: 2em;
+}
+.isActiveLi {
+  background-color: #f5f7fa;
+  color: #409EFF !important;
+  position: relative;
+}
+.isActiveLi::after {
+  width: 3px;
+  content: ' ';
+  background-color: #409EFF;
+  left: 0;
+  bottom: 0;
+  top: 0;
+  position: absolute;
+}
+/deep/ .el-dialog__body {
+  padding: 20px;
+}
+.inputBox {
+  height: 36px;
+  line-height: 36px;
+  padding-right: 30px;
+  -webkit-appearance: none;
+  background-color: #fff;
+  background-image: none;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  color: #606266;
+  display: inline-block;
+  font-size: inherit;
+  width: 336px;
+  outline: 0;
+  padding: 0 15px;
+  cursor: pointer;
+  -webkit-transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.mainEl {
+  .mainHead {
+    margin-bottom: 10px;
+    span:nth-child(2) {
+      color: #409EFF;
+      cursor: pointer;
+    }
+  }
+  .mainTag {
+    height: 400px;
+    overflow-x: hidden;
+    overflow-y: auto;
+    /deep/ .el-tag--medium {
+      background-color: #fff;
+      border: 1px solid #e4e4e4;
+      border-radius: 20px;
+      color: #4c4c4c;
+      margin: 5px 5px 5px 0;
+      max-width: 240px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      position: relative;
+      padding: 0 20px 0 10px;
+    }
+    .el-tag--medium /deep/ i {
+      color: #8f8f8f;
+      position: absolute;
+      right: 5px;
+      top: 5px;
+    }
+    .el-tag--medium /deep/ i:hover {
+      background-color: #9b9b9b;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 240 - 0
src/components/organization/EmployeeTable-useable.vue

@@ -0,0 +1,240 @@
+<template>
+  <div id="app">
+    <h3>可缩放 + 可拖拽审批流</h3>
+
+    <!-- 工具条 -->
+    <div class="toolbar">
+
+      <div class="scale-wrap">
+        缩放:
+        <el-slider v-model="scale" :min="50" :max="150" :step="5" show-input input-size="mini"
+          style="width:200px;display:inline-block; vertical-align:middle; margin-left:10px;"></el-slider>
+      </div>
+    </div>
+
+    <!-- 画布 -->
+    <div class="canvas" :style="{ transform: `scale(${scale / 100})` }" @wheel.passive="onWheel">
+      <!-- 垂直链 -->
+      <div class="chain" :style="{ transform: `translate(${chainX}px, ${chainY}px)` }" draggable="true"
+        @dragstart="chainDragStart" @dragend="chainDragEnd">
+        <!-- 渲染节点 -->
+        <div v-for="(node, i) in list" :key="node.id" class="node-box">
+          <!-- 节点 -->
+          <div class="node">
+            <span class="label">{{ node.name }}</span>
+            <!-- 减号:删节点 -->
+            <span class="btn del" @click="del(i)" v-if="!node.fixed">-</span>
+            <!-- 加号:往后插节点 -->
+            <span class="btn add" @click="add(i)">+</span>
+          </div>
+
+          <!-- 向下连线(最后一个不画) -->
+          <div class="line" v-if="i < list.length - 1"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 调试 -->
+    <pre class="debug">{{ JSON.stringify(list, null, 2) }}</pre>
+  </div>
+</template>
+
+
+<script>
+export default {
+  name: 'App',
+  data() {
+    return {
+      scale: 100,        // 缩放百分比
+      list: [
+        { id: 1, name: '开始', fixed: true },   // 固定节点,不可删
+        { id: 2, name: '结束', fixed: true }
+      ],
+      uid: 3,
+      dragIdx: null,     // 当前拖拽索引
+      dragStartX: 0,     // 拖拽起始偏移
+      dragStartY: 0,
+      /* 整条链的拖拽 */ 
+      chainX: 0,   // 链在画布内的左偏移
+      chainY: 0,
+      chainDrag: false,  // 是否正在拖链
+      chainStartX: 0,    // 拖链起始鼠标位置
+      chainStartY: 0
+    }
+  },
+  created() {
+    // 初始化 3 个节点
+    this.list = [
+      { id: this.uid++, name: '开始', x: 100, y: 50, fixed: true },
+      { id: this.uid++, name: '审批人', x: 100, y: 150 },
+      { id: this.uid++, name: '结束', x: 100, y: 250, fixed: true }
+    ]
+    // 如需要持久化,打开下面注释
+    // const cache = localStorage.getItem('flow')
+    // if (cache) this.list = JSON.parse(cache)
+  },
+  methods: {
+
+    /* 链整体拖拽 */
+  chainDragStart(e) {
+    this.chainDrag = true
+    this.chainStartX = e.clientX - this.chainX
+    this.chainStartY = e.clientY - this.chainY
+    e.dataTransfer.effectAllowed = 'move'
+    e.dataTransfer.setDragImage(new Image(), 0, 0)
+  },
+  chainDragEnd(e) {
+    if (!this.chainDrag) return
+    this.chainX = e.clientX - this.chainStartX
+    this.chainY = e.clientY - this.chainStartY
+    this.chainDrag = false
+    },
+  
+    /* 添加节点 */
+    // 在第 i 个节点“之后”插入新审批节点
+    add(i) {
+      this.list.splice(i + 1, 0, {
+        id: this.uid++,
+        name: '审批人',
+        fixed: false
+      })
+    },
+
+
+    del(i) {
+      this.list.splice(i, 1)
+    },
+
+    /* 拖拽 */
+    dragstart(e, i) {
+      this.dragIdx = i
+      const node = this.list[i]
+      const rect = e.target.getBoundingClientRect()
+      this.dragStartX = e.clientX - node.x * this.scale / 100
+      this.dragStartY = e.clientY - node.y * this.scale / 100
+      e.dataTransfer.effectAllowed = 'move'
+      // 隐藏默认幽灵
+      e.dataTransfer.setDragImage(new Image(), 0, 0)
+    },
+    dragend(e) {
+      const i = this.dragIdx
+      if (i === null) return
+      // 计算新坐标(除以 scale 得到画布内逻辑坐标)
+      this.list[i].x = (e.clientX - this.dragStartX) / (this.scale / 100)
+      this.list[i].y = (e.clientY - this.dragStartY) / (this.scale / 100)
+      this.dragIdx = null
+      // 持久化
+      // localStorage.setItem('flow', JSON.stringify(this.list))
+    },
+    /* 鼠标+Ctrl 缩放 */
+    onWheel(e) {
+      e.preventDefault()
+      const delta = e.deltaY > 0 ? -5 : 5
+      this.scale = Math.max(50, Math.min(150, this.scale + delta))
+    },
+
+    /* 构建 SVG 路径 */
+    buildPath(i) {
+      const a = this.list[i]
+      const b = this.list[i + 1]
+      const x1 = a.x + 60
+      const y1 = a.y + 40
+      const x2 = b.x + 60
+      const y2 = b.y
+      return `M${x1},${y1} C${x1},${y1 + 30} ${x2},${y2 - 30} ${x2},${y2}`
+    }
+  }
+}
+</script>
+
+<style scoped>
+.toolbar {
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.scale-wrap {
+  margin-left: auto;
+}
+
+.canvas {
+  position: relative;
+  width: 100%;
+  height: 70vh;
+  border: 1px solid #dcdfe6;
+  overflow: hidden;
+  transform-origin: 0 0;
+  background: #fafafa;
+}
+
+/* 垂直居中整个链 */
+.chain {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20px 0;
+}
+
+/* 单个节点盒子 */
+.node-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* 节点样式 */
+.node {
+  position: relative;
+  width: 140px;
+  line-height: 40px;
+  text-align: center;
+  border: 1px solid #409eff;
+  border-radius: 4px;
+  background: #fff;
+  cursor: pointer;
+}
+
+/* 加号/减号按钮 */
+.btn {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 20px;
+  height: 20px;
+  line-height: 18px;
+  text-align: center;
+  border-radius: 50%;
+  font-size: 16px;
+  color: #fff;
+  cursor: pointer;
+}
+
+.add {
+  background: #67c23a;
+  right: -30px;
+}
+
+.del {
+  background: #f56c6c;
+  left: -30px;
+}
+
+/* 向下连线 */
+.line {
+  width: 2px;
+  height: 40px;
+  background: #909399;
+  margin: 4px 0;
+}
+
+/* 调试用 */
+.debug {
+  background: #f5f7fa;
+  padding: 10px;
+  margin-top: 20px;
+  font-size: 12px;
+}
+</style>
+

+ 168 - 0
src/components/organization/Process/index.vue

@@ -0,0 +1,168 @@
+<template>
+    <div>
+        <div class="my-nav-content">
+            <section class="dingflow-design" ref="dingflowDesignRef">
+                <div class="zoom">
+                    <div class="zoom-out" @click="zoomOut" title="缩小"></div>
+                    <span>{{ nowVal }}%</span>
+                    <div class="zoom-in" @click="zoomIn" title="放大"></div>
+                    <!--刷新图标代码-->
+                    <div class="zoom-reset" @click="zoomReset" title="还原缩放比例">&#10227</div>
+                </div>
+                <div class="box-scale" ref="boxScaleRef">
+                    <nodeWrap  />
+                    <div class="end-node">
+                        <div class="end-node-circle"></div>
+                        <div class="end-node-text">流程结束</div>
+                    </div>
+                </div>
+            </section>
+        </div>
+    </div>
+</template>
+
+
+
+<script>
+import { resetImage, wheelZoomFunc, zoomInit } from "@/utils/workflow/zoom.js";
+import { nodeTypeList } from '@/utils/workflow/const'
+import NodeWrap from './nodeWrap.vue';
+import { mapActions, mapState } from 'vuex'
+export default {
+    components: {
+        NodeWrap
+    },
+    props: {
+        processData: {
+            type: Object,
+            required: true,
+            default: () => (null),
+        }
+    },
+
+    data() {
+        return {
+            tipList: [],
+            tipVisible: false,
+            nowVal: 100,
+            nodeConfig: {}
+        }
+    },
+
+    watch: {
+        processData: {
+            handler(newVal) {
+                this.nodeConfig = newVal;
+                console.log('process mounted nodeConfig', this.nodeConfig);
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+
+    async mounted() {
+
+        if (this.processData) this.nodeConfig = this.processData;
+
+        console.log('process mounted nodeConfig', this.nodeConfig);
+
+        this.$nextTick(() => {
+            if (!this.$refs.dingflowDesignRef || !this.$refs.boxScaleRef) return;
+            zoomInit(this.$refs.dingflowDesignRef, this.$refs.boxScaleRef, (val) => {
+                this.nowVal = val
+            })
+        });
+    },
+
+
+    methods: {
+
+        ...mapActions('flowDesigner', [
+            'setFlowId',
+            'setIsTried',
+        ]),
+
+
+        zoomIn() {
+            wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 + 0.1, isExternalCall: true })
+        },
+        zoomOut() {
+            wheelZoomFunc({ scaleFactor: parseInt(this.nowVal) / 100 - 0.1, isExternalCall: true })
+        },
+        zoomReset() {
+            resetImage()
+        },
+        /**
+         * 判断流程中是否有审批节点
+         * @param treeNode
+        */
+        preTreeIsApproveNode(treeNode) {
+            if (!treeNode) return false;
+            if (treeNode.nodeType == 4) {
+                return true;
+            } else {
+                return this.preTreeIsApproveNode(treeNode.childNode);
+            }
+        },
+
+        /** 节点验证 */
+        validateErr({ childNode }) {
+            if (childNode) {
+                let { nodeType, error, nodeName, conditionNodes } = childNode;
+                if (nodeType == 1) {
+                    this.validateErr(childNode);
+                } else if (nodeType == 2) {
+                    this.validateErr(childNode);
+                    for (var i = 0; i < conditionNodes.length; i++) {
+                        if (conditionNodes[i].error) {
+                            this.tipList.push({ name: conditionNodes[i].nodeName, nodeType: "条件" });
+                        }
+                        this.validateErr(conditionNodes[i]);
+                    }
+                } else if (nodeType == 3) {
+                    this.validateErr(childNode);
+                } else if (nodeType == 4 || nodeType == 6) {
+                    if (error) {
+                        this.tipList.push({
+                            name: nodeName,
+                            nodeType: nodeTypeList[nodeType],
+                        });
+                    }
+                    this.validateErr(childNode);
+                }
+            } else {
+                childNode = null;
+            }
+        },
+
+        getJson() {
+            this.setIsTried(true);
+            let isApproveNode = this.preTreeIsApproveNode(this.nodeConfig);
+            if (!this.nodeConfig || !this.nodeConfig.childNode || !isApproveNode) {
+                this.$emit('nextChange', { label: "流程设计", key: "processDesign" });
+                return false;
+            }
+            this.tipList = [];
+            this.validateErr(this.nodeConfig);
+            if (this.tipList.length != 0) {
+                this.$emit('nextChange', { label: "流程设计", key: "processDesign" });
+                this.tipVisible = true;
+                return false;
+            }
+            let submitData = JSON.parse(JSON.stringify(this.nodeConfig));
+            return submitData;
+        },
+
+        getData() {
+            return new Promise((resolve, reject) => {
+                let resData = this.getJson();
+                console.log('process resData', resData);
+                if (!resData) {
+                    reject({ formData: null });
+                }
+                resolve({ formData: resData })
+            })
+        }
+    }
+}
+</script>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 157 - 0
src/components/organization/Process/nodeWrap copy.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 22 - 0
src/components/organization/Process/nodeWrap.vue


+ 172 - 0
src/components/organization/workflow.vue

@@ -0,0 +1,172 @@
+<template>
+  <div>
+    <div class="my-nav">
+      <div class="my-nav-left">
+        <div class="my-nav-back">
+          <i class="flowicon flowicon-left"></i>
+        </div>
+        <div class="my-nav-title">{{ title }}</div>
+      </div>
+
+      <div class="my-nav-center">
+        <div class="step-tab">
+          <div v-for="(item, index) in steps" :key="index" class="step"
+            :class="[activeStep == item.key ? 'active' : '']" @click="changeSteps(item)">
+            <span class="step-index">{{ index + 1 }}</span>
+            {{ item.label }}
+          </div>
+          <div class="ghost-step step" :style="{ transform: translateX }"></div>
+        </div>
+      </div>
+
+      <div class="my-nav-right">
+        <!-- <a href="https://gitee.com/ldhnet/AntFlow-Designer" target="_blank">
+        <svg t="1671270284061" class="icon" style="background-color: #fff;border-radius:20px;margin-bottom:-6px;"
+          viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1920" width="32" height="32">
+          <path
+            d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z"
+            fill="#C71D23" p-id="1921"></path>
+        </svg>
+      </a>
+      <a style="margin-right: 40px;" href="https://github.com/ldhnet/AntFlow-Designer" target="_blank">
+        <svg height="32" style="background-color: #fff;border-radius:20px;margin-bottom:-6px;" aria-hidden="true"
+          viewBox="0 0 24 24" version="1.1" width="32" data-view-component="true"
+          class="octicon octicon-mark-github v-align-middle">
+          <path
+            d="M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z">
+          </path>
+        </svg>
+      </a> -->
+        <button type="button" class="flow-btn button-publish" @click="publish">
+          <span>发 布</span>
+        </button>
+      </div>
+
+      <div v-if="processConfig" v-show="activeStep === 'basicSettingDesign'">
+        <basicSetting ref="basicSettingDesign" :basicData="processConfig" @nextChange="changeSteps" />
+      </div>
+      <div v-show="activeStep === 'formDesign'">
+        <dynamicForm ref="formDesign" />
+      </div>
+      <div v-if="nodeConfig" v-show="activeStep === 'processDesign'">
+        <process ref="processDesign" :processData="nodeConfig" @nextChange="changeSteps" />
+      </div>
+    </div>
+  </div>
+
+
+</template>
+
+
+
+<script>
+import {
+  NodeUtils
+} from '@/utils/workflow/nodeUtils';
+import { FormatDisplayUtils } from '@/utils/workflow/formatdisplay_data';
+import { showLoading, closeLoading } from '@/utils/workflow/loading';
+import BasicSetting from './BasicSetting.vue';
+import DynamicForm from './DynamicForm/index.vue';
+import Process from './Process';
+export default {
+    components: {
+      BasicSetting,
+      DynamicForm,
+      Process
+    },
+    data() {
+        return {
+          title: '请假管理',
+          activeStep: 'basicSettingDesign',
+          steps: [
+            { label: "基础设置", key: "basicSettingDesign" },
+            { label: "表单设计", key: "formDesign" },
+            { label: "流程设计", key: "processDesign" }
+          ],
+          processConfig: null,
+          nodeConfig: null,
+          basicSettingDesign: null,
+          formDesign: null,
+          processDesign: null
+        }
+    },
+    computed: {
+      translateX() {
+        return `translateX(${(this.steps.findIndex(t => t.key === this.activeStep) - 1) * 100}%)`
+      }
+  },
+
+  mounted() {
+    // showLoading();
+    let mockjson = NodeUtils.createStartNode();
+    let data = FormatDisplayUtils.getToTree(mockjson.data);
+    this.processConfig = data;
+    this.title = data.bpmnName;
+    this.nodeConfig = data.nodeConfig;
+    // closeLoading();
+  },
+    methods: {
+      changeSteps(item) {
+        this.activeStep = item.key
+      },
+      publish() { }
+    }
+  }
+
+</script>
+
+
+<style lang="scss" scoped>
+
+  .step-tab {
+    display: flex;
+    justify-content: center;
+    position: relative;
+    height: 60px;
+    font-size: 14px;
+    border-right: 0px solid #1583f2;
+    text-align: center;
+    cursor: pointer;
+  }
+
+  .step {
+    width: 140px;
+    line-height: 100%;
+    padding-left: 30px;
+    padding-right: 30px;
+    line-height: 60px;
+    cursor: pointer;
+    position: relative;
+  }
+
+  .step-index {
+    display: inline-block;
+    width: 18px;
+    height: 18px;
+    border: 1px solid #fff;
+    border-radius: 8px;
+    line-height: 18px;
+    text-align: center;
+    box-sizing: border-box;
+  }
+
+  .ghost-step {
+    position: absolute;
+    height: 60px;
+    z-index: -1;
+    background: #4483f2;
+    transition: transform .5s;
+  }
+
+  .ghost-step::after {
+    content: '';
+    border-width: 6px 6px 6px;
+    border-style: solid;
+    border-color: transparent transparent white;
+    position: absolute;
+    bottom: 0;
+    left: 50%;
+    margin-left: -6px;
+  }
+</style>
+

+ 65 - 13
src/newPerformance/components/MyPerformance.vue

@@ -47,6 +47,8 @@
             <div>
                 <el-link v-if="detailInfo" type="primary" @click="examineLogDialogVisible = true"
                     style="margin-right: 10px;">操作日志</el-link>
+                <el-link v-if="detailInfo && allowViewRecords" type="primary" @click="openManageRecord()"
+                    style="margin-right: 10px;">管理记录</el-link>
                 <el-link v-if="detailInfo && allowAddIndicator" type="primary" style="margin-right: 10px;"
                     @click="openAddIndicatorDialog()">添加指标</el-link>
                 <el-link v-if="allowDeleteExamine" type="danger"
@@ -59,9 +61,10 @@
                     <i class="el-icon-d-arrow-right"></i>
                 </div>
             </div>
-
+            
         </div>
-        
+
+
 
         <!-- 考核详情 -->
         <div class="table-box" v-if="detailInfo">
@@ -145,7 +148,6 @@
                 </el-table-column>
 
 
-
                 <el-table-column prop="businessStatus" label="指标考核状态" align="center" min-width="120">
                     <template slot-scope="scope">
                         <div v-if="scope.row.businessStatus == 'end'" class="green-color">已结束</div>
@@ -164,9 +166,9 @@
                     </template>
                 </el-table-column>
 
+
                 <el-table-column label="操作" align="center">
                     <template slot-scope="scope">
-
                         <el-popover ref="popoverRef" placement="bottom" trigger="click">
                             <template #reference>
                                 <el-link slot="reference" type="primary">更多操作</el-link>
@@ -176,6 +178,7 @@
                                     @click="getProcessTracing({ reviewId: scope.row.reviewId, nodes: scope.row.flow.nodes, reviewIndicatorId: scope.row.reviewIndicatorId })">
                                     审批过程
                                 </el-link>
+
                                 <el-link style="margin-bottom: 10px;"
                                     v-if="allowResetIndicator && scope.row.businessStatus !== 'target_confirm' && allowResetIndicator && scope.row.businessStatus !== 'end'"
                                     type="primary" @click="openResetNodeDialog(scope.row)">重置流程</el-link>
@@ -211,16 +214,15 @@
             <div class="fontColorB">暂无考核数据</div>
         </div>
 
-        <!-- 编辑计算公式 -->
-        <!-- 
-            <FormulaComp v-if="currentIndicator" v-model="showFormula"
-                :fixed-props="[{ key: 'target', name: '目标' }, { key: 'weight', name: '权重' }, { key: 'result', name: '结果值' }]"
-                :expressions-props="currentIndicator.expression.formulas || []" :is-edit="false"
-                @onConfirm="onFormulaConfirm" /> 
-        -->
 
 
+        <!-- 管理记录 -->
+        <ManageRecord :showDrawer.sync="showManageRecord" :tableData="tableData" :currentIndex="indicatorIndex"
+            @confirm="recordConfirm">
+        </ManageRecord>
+
 
+        <!-- 公式 -->
         <el-dialog :visible.sync="showFormula" append-to-body :close-on-press-escape="true" center width="600px"
             title="计算公式">
             <div class="scroll-bar" style="max-height: 400px; overflow-y: auto;">
@@ -335,6 +337,9 @@
         <!--  指标okr选择  -->
         <TargetSearch :visible.sync="showIndicatorTargetSearch" :selectedOkrs="indicatorOkrs"
             :selectedPlans="indicatorPlans" :key="`indicatorTarget`" @confirm="onIndicatorOkrSelected" />
+
+        <AddRecordsDialog v-model="addRecordDialog" :selectRecord="selectRecord" :reviewIndicatorId="reviewIndicatorId"
+            @confirm="getRecords(true)" />
     </div>
 </template>
 
@@ -351,6 +356,8 @@ import TargetPlanComp from "@/newPerformance/components/PublicComp/TargetPlanCom
 import ExamineLogDialog from '@/newPerformance/components/MyPerformance/ExamineLog'; // 考核日志
 import ResetNodeDialog from '@/newPerformance/components/MyPerformance/ResetNode'; // 重置流程
 import AddIndicatorDialog from '@/newPerformance/components/MyPerformance/AddIndicator'; // 添加指标
+import AddRecordsDialog from '@/newPerformance/components/MyPerformance/AddRecords'; // 添加管理记录
+import ManageRecord from '@/newPerformance/components/MyPerformance/ManageRecord'; // 添加管理记录
 import VerifyDialog from '@/newPerformance/components/PublicComp/VerifyDialog'; // 验证码
 import Vcode from "vue-puzzle-vcode";
 import TargetSearch from "@/performance/views/assessManagement/TargetSearch";
@@ -368,7 +375,9 @@ export default {
         ResetNodeDialog,
         VerifyDialog,
         Vcode,
-        TargetSearch
+        AddRecordsDialog,
+        TargetSearch,
+        ManageRecord
     },
     props: {
         reviewId: {
@@ -409,6 +418,11 @@ export default {
             levelName: '', // 考核详情信息
             currentIndicator: null,
             formulas: [], //公式列表
+            records: [],
+            showManageRecord: false, // 管理记录
+            
+            addRecordDialog: false,
+            selectRecord: null,
             showFormula: false, // 公式弹框
             employeeMap: this.$getEmployeeMap(), // 员工列表
             employeeId: '',
@@ -546,10 +560,15 @@ export default {
 
         // 被考核人 主管理员,子管理员可以修改 绑定的OKR,计划
         allowEditOkrs() {
-            let isSelf = this.selectedIndicatorInfo && this.selectedIndicatorInfo.employeeId == this.user_info.id
+            let isSelf = this.detailInfo && this.detailInfo.employeeId == this.user_info.id
             return (this.isCreator || this.getRole1 || isSelf)
         },
 
+        allowViewRecords() {
+            let isSelf = this.detailInfo && this.detailInfo.employeeId == this.user_info.id
+            return (this.isCreator || this.getRole1 || this.getRole2 || isSelf)
+        }
+
     },
 
     created() {
@@ -708,6 +727,39 @@ export default {
                 }
             })
         },
+
+        recordConfirm() {
+            this.showManageRecord = false
+        },
+
+        openManageRecord() {
+            this.showManageRecord = true
+        },
+
+        getRecords(is = false) {
+            let url = `/performance/review/indicator/record/${this.user_info.site_id}/${this.reviewIndicatorId}`
+            let params = {
+                page: is ? 1 : this.recordPage,
+                pageSize: this.recordPageSize,
+            }
+            this.$axiosUser('get', url, params).then(res => {
+                this.records = res.data.data.list
+                this.recordTotal = res.data.data.total
+            })
+        },
+
+        handleRecordSizeChange(val) {
+            this.recordPage = 1;
+            this.recordPageSize = val;
+            this.getRecords()
+        },
+
+        handleRecordCurrentChange(val) {
+            this.recordPage = val;
+            this.recordPageSize = 10;
+            this.getRecords()
+        },
+        
         // 过程跟踪
         getProcessTracing(row) {
             this.showData = row;

+ 233 - 0
src/newPerformance/components/MyPerformance/AddRecords.vue

@@ -0,0 +1,233 @@
+<template>
+    <el-dialog :visible.sync="addRecordDialog" append-to-body :close-on-press-escape="true" center width="600px"
+        :title="dialogTitle" @open="init()" :before-close="dialogBeforeClose">
+        <div class="scroll-bar" style="max-height: 400px; overflow-y: auto;">
+            <el-form ref="form" :model="recordForm" :rules="rules" label-width="80px">
+                <el-form-item label="标题" prop="title">
+                    <el-input v-model="recordForm.title" placeholder="请输入标题" clearable></el-input>
+                </el-form-item>
+
+                <el-form-item label="内容" prop="content">
+                    <el-input v-model="recordForm.content" placeholder="请输入内容" clearable></el-input>
+                </el-form-item>
+
+                <el-form-item label="附件">
+                    <uploadOss :key="Date.now()" class="avatar-uploader" :headers="$xtoken" :show-file-list="true"
+                        :multiple="true" :limit="5" :accept="acceptFile" :file-list="fileList" :action="$action"
+                        :on-preview="onFilePreView" :on-success="handleSuccess" :on-remove="handleRemove"
+                        :before-upload="beforeFilesUpload">
+                        <el-button class="primaryBtn" icon="el-icon-paperclip" plain size="mini">上传附件</el-button>
+                        <!-- <div slot="tip" class="el-upload__tip">(支持上传xlsx,xls,doc,docx,pdf,txt,png,jpeg,jpg,gif, 大小不能超过5M)</div> -->
+                    </uploadOss>
+                </el-form-item>
+            </el-form>
+        </div>
+        <div slot="footer">
+            <el-button @click="cancel()">取 消</el-button>
+            <el-button type="primary" @click="confirm()">确 定</el-button>
+        </div>
+    </el-dialog>
+
+</template>
+
+
+<script>
+import uploadOss from '@/components/upload';
+import { _debounce } from '@/utils/auth';
+import { mapGetters } from 'vuex';
+import axios from "axios"
+export default {
+    components: {
+        uploadOss
+    },
+    model: {
+        prop: 'addRecordDialog',
+        event: 'close-dialog'
+    },
+    props: {
+        selectRecord: {
+            type: Object,
+            default: null
+        },
+        addRecordDialog: {
+            type: Boolean,
+            default: false
+        },
+        reviewIndicatorId: {
+            type: Number | String,
+            default: ''
+        },
+    },
+    data() {
+        return {
+            dialogTitle: "新建管理记录",
+            recordForm: {
+                title: "",
+                content: "",
+                files: []
+            },
+            rules: {
+                title: [
+                    { required: true, message: "请选择标题", trigger: "blur" },
+                ],
+
+                content: [
+                    { pattern: /^[A-Za-z0-9\-::,.!。!,;;、/_ ()()'"“”\n\u4e00-\u9fa5]{0, 50}$/, message: '内容不支持特殊字符', trigger: 'blur' }
+                ],
+            },
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF',
+            fileList: [], // 附件列表
+            uploadFileList: [], // 上传成功的fileList
+            imgUrl: ""
+        }
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+    },
+
+    methods: {
+        init() {
+            if (this.selectRecord) {
+                this.dialogTitle = "编辑管理记录"
+                this.recordForm = this.selectRecord
+                this.batchLoad()
+            } else {
+                this.dialogTitle = "新建管理记录"
+                this.recordForm = {
+                    title: "",
+                    content: "",
+                    files: []
+                };
+            }
+            
+        },
+
+        dialogBeforeClose() {
+            this.$emit('close-dialog', false)
+        },
+
+        cancel() {
+            this.recordForm = {
+                title: "",
+                content: "",
+                files: []
+            };
+            this.$emit('close-dialog', false)
+        },
+
+        confirm() {
+            reviewIndicatorRecordId
+            let url = `/performance/review/indicator/record/create/${this.user_info.site_id}/${this.reviewIndicatorId}`
+            let data = { ...this.recordForm }
+            data.files = this.uploadFileList
+            this.$http.post(url, data).then(res => {
+                if (res.code == 1) {
+                    this.$emit('close-dialog', false)
+                    this.$emit('confirm')
+                }
+            })
+        },
+
+        onFilePreView(file) {
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            let lastIndex = file.url && file.url.lastIndexOf("/") || -1
+            let suffix; //文件后缀名
+
+            if (lastIndex > 0) {
+                suffix = file.url.substr(lastIndex + 1, file.url.length - 1).split(".")[1];
+                if (imgFiles.includes(suffix)) {
+                    this.imgUrl = ''
+                    this.imgUrl = file.url;
+                    this.$viewerApi({
+                        images: [this.imgUrl]
+                    })
+                } else {
+                    window.open(file.url, '_blank');
+                }
+            }
+
+        },
+
+        /* 主函数:接收 url 数组 → 批量回显 */
+        async batchLoad() {
+            const urlArr = this.recordForm && this.recordForm.files && this.recordForm.files.length > 0 ? this.recordForm.files : []; // 已成功上传的文件列表
+            if (!urlArr.length) return
+
+            /* 并发下载并转成 File */
+            const taskList = urlArr.map(u => this.url2File(u))
+            try {
+                const files = await Promise.all(taskList)
+                this.fileList = files // 一次性塞给 el-upload
+                console.log(this.fileList)
+                // this.$message.success(`已回显 ${files.length} 个文件`)
+            } catch (e) {
+                this.$message.error('下载失败:' + e.message)
+            }
+        },
+
+        /* 单条 url → File → el-upload 对象 */
+        async url2File(url) {
+            // 1. 下载成 blob(axios 写法,IE 可用)
+            const { data: blob, headers } = await axios.get(url, {
+                responseType: 'blob'
+            })
+
+            // 2. 从 Content-Disposition 或 url 取文件名
+            let fileName = 'unknown'
+            const disposition = headers['content-disposition']
+            if (disposition && disposition.includes('filename=')) {
+                fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''))
+            } else {
+                const temp = url.split('/').pop().split('?')[0]
+                fileName = decodeURIComponent(temp)
+            }
+
+            // 3. 生成 File 对象
+            const file = new File([blob], fileName, { type: blob.type })
+
+            // 4. 返回 el-upload 需要的格式
+            return {
+                name: fileName,
+                url: url,        // 预览图
+                raw: file,       // File 对象
+                status: 'success'
+            }
+        },
+
+
+        handleSuccess: _debounce(function (response, file, fileList) {
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+
+            // this.comfirmUploadFiles(this.uploadFileList);
+        }),
+
+        handleRemove(file, fileList) {
+            this.fileList = fileList; // 用来显示的文件列表
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+            // this.comfirmUploadFiles(this.uploadFileList);
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+
+    }
+}
+</script>

+ 1 - 1
src/newPerformance/components/MyPerformance/ExamineLog.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-dialog title="考核操作日志" :visible.sync="examineLogDialogVisible" width="800px" :before-close="dialogBeforeClose"
+    <el-dialog title="考核操作日志" :visible.sync="examineLogDialogVisible" width="700px" :before-close="dialogBeforeClose"
         append-to-body>
         <div class="all">
 

+ 746 - 0
src/newPerformance/components/MyPerformance/ManageRecord copy.vue

@@ -0,0 +1,746 @@
+<template>
+    <div>
+        <el-drawer title="跟踪管理记录" :visible.sync="showDrawerTow" append-to-body direction="btt" class="all-derawer"
+            :before-close="handleClose" :close-on-press-escape="false">
+            <div class="flex-box" style="height: calc(100vh - 30%);">
+                <div class="drawer-left scroll-bar">
+                    <template v-if="tableData && tableData.length > 0">
+                        <div class="draft-content" v-for="(item, index) in tableData" :key="index">
+                            <div class="drawer-left-title font-flex-word" @click="chooseIndicator(item, index)">{{ item.title }}</div>
+                            <!-- {{ tableData }} -->
+                            <!-- <div class="drawer-item  flex-box-ce"
+                                :class="[activeDrawerIndex == index + '-' + index2 ? 'active-drawer-item' : '']"
+                                v-for="(item2, index2) in item.index" :key="index2"
+                                @click="activeItem(index, index2, item2)">
+                                <div style="width:200px;" class="font-flex-word">{{ item2.name }}</div>
+                                <div style="padding: 0 10px;" class="blue">({{ item2.mamage_record.length }})</div>
+                            </div> -->
+                        </div>
+                    </template>
+
+                </div>
+
+                <div class="drawer-right flex-box-v scroll-bar flex-1" style="overflow: auto; background-color: #fff;">
+                    <div class="drawer-right-header">
+                        <div class="drawer-right-title" v-if="tableData && tableData.length > 0 ">{{ tableData[currentIndex].title }}</div>
+                        <div class="orange" style="margin: 10px 0;">考核标准</div>
+                        <div class="remark" v-if="tableData && tableData.length > 0">{{ tableData[currentIndex].content }}</div>
+                    </div>
+                    <div class="drawer-right-main">
+                        <div class="flex-box-ce">
+                            <div class="flex-1" style="font-size: 16px;">跟踪管理记录</div>
+                            <el-button type="primary" size="mini" @click="addPlan()">新增</el-button>
+                        </div>
+                    </div>
+                    <div class="flex-box flex-1" v-if="records.length > 0">
+                        <div class="plan-left flex-3">
+                            <div class="plan-left-content" @click="activePlan(item, index)"
+                                :class="[activePlanIndex == index ? 'active-drawer-item' : '']"
+                                v-for="(item, index) in records" :key="index">
+                                <div class="flex-box-ce">
+                                    <div class="plan-right-name flex-1" v-if="item.employeeId">
+                                        记录人:{{ $getEmployeeMapItem(item.employeeId).name }}</div>
+                                    <template v-if="item.images">
+                                        <i v-if="item.images.length > 0" class="el-icon-picture fontColorB"
+                                            style="font-size: 18px;padding-right: 10px;"></i>
+                                    </template>
+                                    <i v-if="item.append" class="el-icon-paperclip fontColorB"
+                                        style="font-size: 18px;padding-right: 10px;"></i>
+                                    <div class="fontColorB">{{ item.createTime }}</div>
+                                </div>
+                                <div class="plan-title">{{ item.title }}</div>
+                            </div>
+                        </div>
+                        <div class="plan-right flex-2">
+                            <div class="flex-box-end btns" >
+                                <el-button class="dangerBtn" size="mini" round plain
+                                    @click="deletePlan()">删除</el-button>
+                                <el-button class="primaryBtn" size="mini" round @click="saveActionPlan()">提交</el-button>
+                            </div>
+                            <div class="plan-right-date fontColorB">{{ planData.createTime }}</div>
+                            <el-input type="textarea" @blur="inputBlur" @focus="inputFocus" @input="inputText"
+                                class="plan-right-title" rows="6" clearable ref="inputTitle" placeholder="请输入跟踪管理记录内容"
+                                v-model="planData.title" maxlength="50" show-word-limit></el-input>
+                            <el-input ref="inputTitle2" type="textarea" @blur="inputBlur" @focus="inputFocus"
+                                @input="inputText" class="plan-textarea" rows="5" maxlength="50" show-word-limit
+                                clearable placeholder="选填,请输入备注" v-model="planData.content"></el-input>
+
+                            <div style="height: 300px;">
+                                <uploadOss :key="Date.now()" class="avatar-uploader" :headers="$xtoken"
+                                    :show-file-list="true" :multiple="true" :limit="5" :accept="acceptFile"
+                                    :file-list="fileList" :action="$action" :on-preview="onFilePreView"
+                                    :on-success="handleSuccess" :on-remove="handleRemove"
+                                    :before-upload="beforeFilesUpload">
+                                    <el-button class="primaryBtn" icon="el-icon-paperclip" plain
+                                        size="mini">上传附件</el-button>
+                                    <!-- <div slot="tip" class="el-upload__tip">(支持上传xlsx,xls,doc,docx,pdf,txt,png,jpeg,jpg,gif, 大小不能超过5M)</div> -->
+                                </uploadOss>
+                            </div>
+
+                        </div>
+                    </div>
+                    <div v-else style="background-color: #fff;">
+                        <el-alert title="跟踪管理记录用来做什么?" type="info" description show-icon>
+                            <div class="alert-text">
+                                1、填写工作执行过程的实际情况、表现好坏及奖惩情况
+                                <br />
+                                2、填写管理指导的过程记录
+                                <br />
+                                3、填写的内容可以给评分人作为参考
+                            </div>
+                        </el-alert>
+                        <NoData content="暂无跟踪管理记录" imgW="200px" imgH="130px">
+                            <el-button type="primary" round style="width: 100px;margin: 0 auto;margin-top: 10px;"
+                                @click="addPlan()">
+                                <i class="el-icon-plus"></i>
+                                新建
+                            </el-button>
+                        </NoData>
+                    </div>
+                </div>
+            </div>
+        </el-drawer>
+        <el-dialog title="图片查看" :visible.sync="isShowImg" width="50%">
+            <img :src="imgUrl" style="width: 100%;">
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="isShowImg = false">取 消</el-button>
+                <el-button type="primary" @click="isShowImg = false">确 定</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import uploadOss from '@/components/upload';
+import { _debounce } from '@/utils/auth';
+import axios from "axios"
+
+export default {
+    components: { uploadOss },
+    name: 'TrackManagement',
+    props: {
+        id: {
+            //员工考核记录的ID
+            type: Number,
+            default: 0
+        },
+        // 是否显示组件
+        showDrawer: {
+            type: Boolean,
+            default: false
+        },
+        //数据源
+        tableData: {
+            type: Array,
+            default: () => {
+                return [];
+            }
+        },
+
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+    },
+    data() {
+        return {
+            drawer: false,
+            activeDrawerIndex: '0-0',
+            actionPlanList: [],
+            indexItem: {
+                //选中的指标
+                mamage_record: []
+            },
+            activePlanIndex: 0,
+            planData: {
+                //执行计划(单个)
+                title: '',
+                employee_id: '',
+                images: [],
+                append: '', //附件
+                appendName: '' //附件名称
+            },
+            planData2: {}, //备用数据,当不修改时使用
+            showDrawerTow: false,
+            isFill: false, //是否填写了
+            isFill2: false, //添加执行计划时用,判断是要清空添加的,还是还原编辑的数据
+            isShowAdd: false,
+            isOperation: false, //判断是否能操作执行计划
+            selectItemIndex: 0, //维度当前下标
+
+            isOperationTwo: true, //管理记录人是否能操作指定指标
+            imgs: [],
+            doc: '',
+            isShowImg: false,
+            imgUrl: '',
+            recordPage: 1,
+            recordPageSize: 10,
+            recordTotal: 0,
+            records: [],
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF',
+            fileList: [], // 附件列表
+            uploadFileList: [], // 上传成功的fileList
+            imgUrl: "",
+            reviewIndicatorId: "",
+            currentIndex: 0,
+        };
+    },
+    watch: {
+        async showDrawer(val) {
+            this.showDrawerTow = val;
+            this.getRecords(true)
+        }
+    },
+    methods: {
+
+        chooseIndicator(item, index) {
+            if (this.currentIndex == index) return
+            this.activePlanIndex = 0
+            if (this.isFill) {
+                this.$confirm('请确认提交跟踪记录:' + this.planData.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                })
+                .then(() => {
+                    this.saveActionPlan(() => {
+                        this.setActivePlan(this.records[this.activePlanIndex], index);
+                    });
+                })
+                .catch(() => {
+                    if (this.isFill2) {
+                        this.records.splice(this.activePlanIndex, 1, this.records[this.activePlanIndex]);
+                        this.setActiveItem(this.records[this.activePlanIndex], index);
+                    } else {
+                        this.records[this.activePlanIndex] = this.planData2;
+                        this.planData = this.planData2;
+                        this.$nextTick(() => {
+                            setTimeout(() => {
+                                this.setActivePlan(this.records[this.activePlanIndex], index);
+                            }, 200);
+                        });
+                    }
+                });
+            } else {
+                this.setActivePlan(this.records[this.activePlanIndex], index);
+            }
+            this.currentIndex = index;
+            this.getRecords(true);
+        },
+
+        getRecords(is = false) {
+            let reviewIndicatorId = this.tableData[this.currentIndex].reviewIndicatorId
+            let url = `/performance/review/indicator/record/${this.user_info.site_id}/${reviewIndicatorId}`
+            let params = {
+                page: is ? 1 : this.recordPage,
+                pageSize: this.recordPageSize,
+            }
+            this.$axiosUser('get', url, params).then(res => {
+                this.records = res.data.data.list
+                this.recordTotal = res.data.data.total
+                this.planData = this.records[0]
+                this.reviewIndicatorId = this.tableData[this.currentIndex].reviewIndicatorId
+                this.batchLoad()
+            })
+        },
+        
+
+        // 图片上传
+        onFilePreView(file) {
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            let lastIndex = file.url && file.url.lastIndexOf("/") || -1
+            let suffix; //文件后缀名
+
+            if (lastIndex > 0) {
+                suffix = file.url.substr(lastIndex + 1, file.url.length - 1).split(".")[1];
+                if (imgFiles.includes(suffix)) {
+                    this.imgUrl = ''
+                    this.imgUrl = file.url;
+                    this.$viewerApi({
+                        images: [this.imgUrl]
+                    })
+                } else {
+                    window.open(file.url, '_blank');
+                }
+            }
+
+        },
+
+        /* 主函数:接收 url 数组 → 批量回显 */
+        async batchLoad() {
+            const urlArr = this.planData && this.planData.files && this.planData.files.length > 0 ? this.planData.files : []; // 已成功上传的文件列表
+            if (!urlArr.length) {
+                this.fileList = []
+                return
+            } 
+
+            /* 并发下载并转成 File */
+            const taskList = urlArr.map(u => this.url2File(u))
+            try {
+                const files = await Promise.all(taskList)
+                this.fileList = files // 一次性塞给 el-upload
+                // this.$message.success(`已回显 ${files.length} 个文件`)
+            } catch (e) {
+                this.$message.error('下载失败:' + e.message)
+            }
+        },
+
+        /* 单条 url → File → el-upload 对象 */
+        async url2File(url) {
+            // 1. 下载成 blob(axios 写法,IE 可用)
+            const { data: blob, headers } = await axios.get(url, {
+                responseType: 'blob'
+            })
+
+            // 2. 从 Content-Disposition 或 url 取文件名
+            let fileName = 'unknown'
+            const disposition = headers['content-disposition']
+            if (disposition && disposition.includes('filename=')) {
+                fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''))
+            } else {
+                const temp = url.split('/').pop().split('?')[0]
+                fileName = decodeURIComponent(temp)
+            }
+
+            // 3. 生成 File 对象
+            const file = new File([blob], fileName, { type: blob.type })
+
+            // 4. 返回 el-upload 需要的格式
+            return {
+                name: fileName,
+                url: url,        // 预览图
+                raw: file,       // File 对象
+                status: 'success'
+            }
+        },
+
+
+        handleSuccess: _debounce(function (response, file, fileList) {
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+
+            // this.comfirmUploadFiles(this.uploadFileList);
+        }),
+
+        handleRemove(file, fileList) {
+            this.fileList = fileList; // 用来显示的文件列表
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+            // this.comfirmUploadFiles(this.uploadFileList);
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+        deletePlan() {
+            this.$confirm('确认删除跟踪记录:' + this.planData.title + '?', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                let url = `/performance/review/indicator/record/remove/${this.user_info.site_id}/${this.planData.reviewIndicatorRecordId}`
+                this.$axiosUser('post', url).then(res => {
+                    if (res.data.code == 1) {
+                        this.$message({ message: '删除成功', type: 'success' });
+                        this.recordPage = 1
+                        this.getRecords(true);
+                        this.activePlanIndex = 0
+                    }
+
+                });
+            })
+            .catch(() => { });
+        },
+
+        // 添加
+        addPlan() {
+            this.fileList = []; // 图片附件
+            this.uploadFileList = []; //文件附件
+            let obj = {
+                createTime: "",
+                employeeId: this.user_info.id,
+                title: '',
+                content: "",
+                files: [],
+            };
+            this.records.unshift(obj);
+            if (this.records.length == 1) {
+                this.planData = this.records[0];
+                this.$nextTick(() => {
+                    this.$refs.inputTitle.focus();
+                    this.isFill = true;
+                });
+            } else {
+                this.activePlanIndex = 0;
+                this.planData = this.records[0];
+                this.$nextTick(() => {
+                    this.$refs.inputTitle.focus();
+                    this.isFill = true;
+                });
+            }
+            this.isFill2 = true; // 清空添加
+        },
+
+        // 提交执行计划
+        saveActionPlan(f = function () { }) {
+            if (!this.planData.title) {
+                this.$message.error('请输入跟踪管理记录内容');
+                return false;
+            }
+            
+            this.isFill = false;
+            let data = {
+                title: this.planData.title,
+                content: this.planData.content,
+                files: this.uploadFileList
+            };
+            if (this.planData.reviewIndicatorRecordId) {
+                let url = `/performance/review/indicator/record/edit/${this.user_info.site_id}/${this.planData.reviewIndicatorRecordId}`
+                this.$http.post(url, data).then(res => {
+                    if (res.code == 1) {
+                        this.$message.success('填写成功');
+                        f();
+                    }
+                }) 
+            } else {
+                let url = `/performance/review/indicator/record/create/${this.user_info.site_id}/${this.reviewIndicatorId}`
+                this.$http.post(url, data).then(res => {
+                    if (res.code == 1) {
+                        this.$message.success('填写成功');
+                        f();
+                    }
+                }) 
+            }
+
+        },
+        // 点击执行计划
+        activePlan(item, index) {
+            if (this.activePlanIndex == index) {
+                return false;
+            }
+            if (this.isFill) {
+                this.$confirm('请确认提交跟踪记录:' + this.planData.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                })
+                .then(() => {
+                    this.saveActionPlan(() => {
+                        this.setActivePlan(item, index);
+                    });
+                })
+                .catch(() => {
+                    if (this.isFill2) {
+                        this.indexItem.mamage_record.splice(this.activePlanIndex, 1);
+                        this.setActivePlan(item, index);
+                    } else {
+                        this.indexItem.mamage_record[this.activePlanIndex] = this.planData2;
+                        this.planData = this.planData2;
+                        this.$nextTick(() => {
+                            setTimeout(() => {
+                                this.setActivePlan(item, index);
+                            }, 200);
+                        });
+                    }
+                });
+            } else {
+                this.planData2 = JSON.parse(JSON.stringify(item));
+                this.setActivePlan(item, index);
+            }
+        },
+
+        setActivePlan(item, index) {
+            console.log(item)
+            this.planData = item;
+            this.planData2 = JSON.parse(JSON.stringify(item));
+            // this.fileList = [];
+            // this.uploadFileList = [];
+            this.batchLoad();
+            this.activePlanIndex = index;
+            this.isFill = false;
+            this.isFill2 = false;
+        },
+
+        setActiveItem(index, index2) {
+            this.getShowData(index, index2);
+            this.activeDrawerIndex = index + '-' + index2;
+            this.activePlanIndex = 0;
+            this.isFill = false;
+            this.isFill2 = false;
+        },
+
+        // 点击指标
+        activeItem(index, index2, item) {
+            this.selectItemIndex = item.dimension_xb;
+            this.isOperationTwo = item.isOperation;
+            if (this.activeDrawerIndex == index + '-' + index2) {
+                return false;
+            }
+            if (this.isFill) {
+                this.$confirm('请确认提交跟踪记录:' + this.planData.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                })
+                    .then(() => {
+                        this.saveActionPlan(() => {
+                            this.setActiveItem(index, index2);
+                        });
+                    })
+                    .catch(() => {
+                        if (this.isFill2) {
+                            this.indexItem.mamage_record.splice(this.activePlanIndex, 1);
+                            this.setActiveItem(index, index2);
+                        } else {
+                            this.indexItem.mamage_record[this.activePlanIndex] = this.planData2;
+                            this.planData = this.planData2;
+                            this.$nextTick(() => {
+                                setTimeout(() => {
+                                    this.setActiveItem(index, index2);
+                                }, 200);
+                            });
+                        }
+                    });
+            } else {
+                this.setActiveItem(index, index2);
+            }
+        },
+
+        // 设置选中数据
+        getShowData(index, index2) {
+            if (index != undefined) {
+                this.indexItem = this.actionPlanList[index].index[index2];
+                this.planData = this.indexItem.mamage_record[0];
+                this.setFlie();
+                this.isOperationTwo = this.indexItem.isOperation;
+                if (this.planData) {
+                    this.planData2 = JSON.parse(JSON.stringify(this.planData));
+                }
+            } else {
+                if (this.planIndex.length > 0) {
+                    //如果有指定下标展示
+                    this.indexItem = this.actionPlanList[this.planIndex[0]].index[this.planIndex[1]];
+                    this.planData = this.indexItem.mamage_record[0];
+                    this.setFlie();
+                    this.selectItemIndex = this.planIndex[0];
+                    this.isOperationTwo = this.indexItem.isOperation;
+                    if (this.planData) {
+                        this.planData2 = JSON.parse(JSON.stringify(this.planData));
+                    }
+                    this.activeDrawerIndex = this.planIndex[0] + '-' + this.planIndex[1];
+                } else {
+                    this.indexItem = this.actionPlanList[0].index[0];
+                    this.planData = this.indexItem.mamage_record[0];
+                    this.setFlie();
+
+                    this.isOperationTwo = this.indexItem.isOperation;
+                    if (this.planData) {
+                        this.planData2 = JSON.parse(JSON.stringify(this.planData));
+                    }
+                }
+            }
+        },
+        setFlie() {
+            this.img_fileList = [];
+            this.file_fileList = [];
+            if (this.planData) {
+                this.img_fileList = this.planData.images;
+                if (this.planData.files) {
+                    this.file_fileList = [{ name: this.planData.append_name, url: this.planData.append }];
+                }
+            }
+        },
+
+        handleClose(done) {
+            this.$emit('update:showDrawer', false);
+            if (this.isCz) {
+                this.$emit('confirm');
+            }
+            this.activeDrawerIndex = '0-0';
+            this.activePlanIndex = 0;
+            this.showDrawerTow = false;
+            this.isFill = false; //是否填写了
+            this.isFill2 = false; //添加执行计划时用,判断是要清空添加的,还是还原编辑的数据
+            this.isShowAdd = false;
+            done();
+        },
+
+        // 监听是否填写
+        inputText(val) {
+            if (!this.isFill) {
+                this.isFill = true;
+            }
+        },
+        inputBlur() {
+            this.isShowAdd = false;
+        },
+        inputFocus() {
+            this.isShowAdd = true;
+        }
+    }
+};
+</script>
+
+<style scoped="scoped">
+::v-deep .el-icon-picture-outline {}
+
+.all-derawer ::v-deep.el-drawer {
+    height: 70% !important;
+    border-radius: 20px 20px 0 0;
+    background-color: #f5f7fa;
+    min-width: 1100px !important;
+}
+
+.all-derawer ::v-deep.el-drawer__body::-webkit-scrollbar {
+    display: none !important;
+}
+
+.all-derawer ::v-deep.el-drawer__header {
+    background-color: #f5f7fa;
+    padding: 12px 20px;
+    margin-bottom: 0px;
+    font-size: 16px;
+}
+
+::v-deep .el-upload-list__item-name {
+    max-width: 250px;
+}
+
+::v-deep :focus {
+    outline: 0;
+}
+
+.btns {
+    position: absolute;
+    right: 20px;
+    top: 16px;
+    z-index: 999;
+}
+
+.plan-right-name {
+    color: #222;
+}
+
+.plan-right {
+    padding: 20px;
+    position: relative;
+    background-color: #f5f7fa;
+    min-height: 600px;
+}
+
+.plan-right-date {
+    text-align: center;
+    position: relative;
+    margin-bottom: 30px;
+}
+
+.plan-textarea ::v-deep textarea {
+    background-color: #f5f7fa;
+    border: none;
+}
+
+.plan-right-title {
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.plan-right-title ::v-deep textarea {
+    background-color: #f5f7fa;
+    border: none;
+}
+
+.drawer-left {
+    background-color: #fff;
+    padding: 0 15px;
+    width: 280px;
+    border-right: 1px solid #f1f1f1;
+    overflow: auto;
+    padding-bottom: 60px;
+}
+
+.drawer-left-title {
+    padding: 16px 0;
+    font-weight: 600;
+    font-size: 16px;
+    color: #222;
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.drawer-item {
+    padding: 16px 0;
+    cursor: pointer;
+}
+
+.drawer-item:hover {
+    background-color: #f5f7fa;
+}
+
+.drawer-left .active-drawer-item {
+    background-color: #f5f7fa;
+    border-right: 2px solid #409EFF;
+}
+
+.plan-left-content:hover {
+    background-color: #f5f7fa;
+}
+
+.plan-left .active-drawer-item {
+    background-color: #f5f7fa;
+}
+
+.drawer-right-header {
+    background-color: #fff;
+    border-bottom: 1px solid #f1f1f1;
+    padding: 20px;
+    box-sizing: border-box;
+}
+
+.drawer-right-main {
+    padding: 0px 20px;
+    height: 50px;
+    line-height: 50px;
+    background-color: #fff;
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.plan-title {
+    font-size: 14px;
+    color: #666666;
+    margin-top: 5px;
+}
+
+.plan-left {
+    background-color: #fff;
+    border-right: 1px solid #f1f1f1;
+    overflow: auto;
+}
+
+.remark {
+    height: 60px;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 6;
+    overflow: hidden;
+}
+
+.plan-left-content {
+    border-bottom: 1px solid #f1f1f1;
+    padding: 10px 16px;
+    cursor: pointer;
+    width: 100%;
+}
+</style>

+ 670 - 0
src/newPerformance/components/MyPerformance/ManageRecord.vue

@@ -0,0 +1,670 @@
+<template>
+    <div>
+        <el-drawer title="跟踪管理记录" :visible.sync="showDrawerTow" append-to-body direction="btt" class="all-derawer"
+            :before-close="handleClose" :close-on-press-escape="false">
+            <div class="flex-box" style="height: 100%;">
+                <div class="drawer-left scroll-bar">
+                    <template v-if="indicators && indicators.length > 0">
+                        <div class="draft-content" v-for="(item, index) in indicators" :key="index"
+                            @click="selectIndicator(item)">
+                            <div class="drawer-left-title font-flex-word"
+                                :class="{ 'active': currentIndicator && currentIndicator.reviewIndicatorId === item.reviewIndicatorId }">
+                                {{ item.title }}</div>
+                        </div>
+                    </template>
+
+                </div>
+
+                <div class="drawer-right flex-box-v flex-1">
+                    <div class="drawer-right-header">
+                        <div class="drawer-right-title" v-if="currentIndicator">{{ currentIndicator.title }}</div>
+                        <div class="orange" style="margin: 10px 0;">考核标准</div>
+                        <div class="remark" v-if="currentIndicator">{{ currentIndicator.content }}</div>
+                    </div>
+                    <div class="drawer-right-main">
+                        <div class="flex-box-ce">
+                            <div class="flex-1" style="font-size: 16px;">跟踪管理记录</div>
+                            <el-button v-if="!isAllowAdd" type="primary" size="mini" @click="addRecord()">新增</el-button>
+                        </div>
+                    </div>
+                    <template v-if="records.length > 0">
+                        <div class="flex-box flex-1 scroll-bar" style="overflow: auto; background-color: #fff;">
+                            <div class="plan-left flex-3">
+                                <div class="plan-left-content" v-for="(item, index) in records" :key="index"
+                                    :class="{ 'active-drawer-item': currentRecord && currentRecord.reviewIndicatorRecordId === item.reviewIndicatorRecordId }"
+                                    @click="selectRecord(item)">
+                                    <div class="flex-box-ce">
+                                        <div class="plan-right-name flex-1" v-if="item.employeeId">
+                                            记录人:{{ $getEmployeeMapItem(item.employeeId).name }}</div>
+                                        <div class="fontColorB">{{ item.createTime }}</div>
+                                    </div>
+                                    <div class="plan-title">{{ item.title }}</div>
+                                </div>
+                            </div>
+                            <div class="plan-right flex-2" v-if="currentRecord">
+                                <div class="flex-box-end btns">
+                                    <el-button class="dangerBtn" size="mini" round plain
+                                        @click="deleteRecord()">删除</el-button>
+                                    <el-button class="primaryBtn" size="mini" round
+                                        @click="submitRecord()">提交</el-button>
+                                </div>
+                                <div class="plan-right-date fontColorB">{{ currentRecord.createTime }}</div>
+                                <el-input type="textarea" class="plan-right-title" rows="6" clearable ref="inputTitle"
+                                    placeholder="请输入管理记录标题" v-model="currentRecord.title" maxlength="50"
+                                    show-word-limit></el-input>
+                                <el-input ref="inputTitle2" type="textarea" class="plan-textarea" rows="5"
+                                    maxlength="100" show-word-limit clearable placeholder="请输入管理记录内容"
+                                    v-model="currentRecord.content"></el-input>
+
+                                <div style="height: 300px;">
+                                    <!-- <uploadOss :key="Date.now()" class="avatar-uploader" :headers="$xtoken"
+                                        :show-file-list="true" :multiple="true" :limit="5" :accept="acceptFile"
+                                        :file-list="fileList" :action="$action" :on-preview="onFilePreView"
+                                        :on-success="handleSuccess" :on-remove="handleRemove"
+                                        :before-upload="beforeFilesUpload">
+                                        <el-button class="primaryBtn" icon="el-icon-paperclip" plain
+                                            size="mini">上传附件</el-button>
+                                    </uploadOss> -->
+                                    <el-button style="margin-bottom: 10px;" class="primaryBtn" icon="el-icon-paperclip"
+                                        plain size="mini" @click="fileUploadVisible = true">上传附件</el-button>
+
+                                    <template v-if="currentRecord.files.length > 0">
+                                        <div class="file-item" v-for="file in currentRecord.files" :key="file">
+                                            <el-link type="primary" @click="onFilePreView2(file)">
+                                                <i class="el-icon-document"></i>
+                                                {{ parseUrlFile(file).fileName + parseUrlFile(file).ext }}
+                                            </el-link>
+                                        </div>
+                                    </template>
+
+                                </div>
+
+
+                            </div>
+                        </div>
+
+                        <div class="pagination-box" style="">
+                            <el-pagination @size-change="handleRecordSizeChange"
+                                @current-change="handleRecordCurrentChange" :current-page.sync="recordPage"
+                                :page-size="recordPageSize" layout="total, prev, pager, next" :total="recordTotal">
+                            </el-pagination>
+                        </div>
+                    </template>
+
+
+                    <div v-else style="background-color: #fff;">
+                        <el-alert title="跟踪管理记录用来做什么?" type="info" description show-icon>
+                            <div class="alert-text">
+                                1、填写工作执行过程的实际情况、表现好坏及奖惩情况
+                                <br />
+                                2、填写管理指导的过程记录
+                                <br />
+                                3、填写的内容可以给评分人作为参考
+                            </div>
+                        </el-alert>
+                        <NoData content="暂无跟踪管理记录" imgW="200px" imgH="130px">
+                            <el-button type="primary" round style="width: 100px;margin: 0 auto;margin-top: 10px;"
+                                @click="addRecord()">
+                                <i class="el-icon-plus"></i>
+                                新建
+                            </el-button>
+                        </NoData>
+                    </div>
+
+                    <!-- 文件上传/预览组件 -->
+                    <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :files="currentRecord.files"
+                        :isAllowUpload="false" :ownerId="user_info.id" @confirm="confirmAppendFile" />
+                </div>
+            </div>
+        </el-drawer>
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
+    </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import uploadOss from '@/components/upload';
+import AppendFileUpload from '@/okr/views/targetBusiness/components/AppendFileUpload.vue';
+import previewFile from '@/okr/views/targetBusiness/components/previewFile.vue';
+// 工具:深比较(含 File 对象)
+function deepEqual(a, b) {
+    if (a === b) return true;                        // 同一引用
+    if (a instanceof File && b instanceof File) {    // 文件特殊处理
+        return a.name === b.name && a.size === b.size && a.type === b.type;
+    }
+    if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
+    if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false;
+    if (Array.isArray(a) !== Array.isArray(b)) return false;
+
+    const keysA = Object.keys(a);
+    const keysB = Object.keys(b);
+    if (keysA.length !== keysB.length) return false;
+    for (const key of keysA) {
+        if (!keysB.includes(key)) return false;
+        if (!deepEqual(a[key], b[key])) return false;
+    }
+    return true;
+}
+export default {
+    components: { uploadOss, AppendFileUpload, previewFile },
+    name: 'ManageRecord',
+    props: {
+        currentIndex: {
+            type: Number,
+            default: 0
+        },
+        // 是否显示组件
+        showDrawer: {
+            type: Boolean,
+            default: false
+        },
+        //数据源
+        tableData: {
+            type: Array,
+            default: () => {
+                return [];
+            }
+        },
+
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+        isAllowAdd() {
+            return this.records.some(record => record.reviewIndicatorRecordId == null || record.reviewIndicatorRecordId == '')
+        }
+    },
+    data() {
+        return {
+            drawer: false,
+            showDrawerTow: false,
+            recordPage: 1,
+            recordPageSize: 10,
+            recordTotal: 0,
+            indicators: [],
+            records: [],
+            currentIndicator: null,
+            currentRecord: null,
+            originalRecord: null,
+            showConfirmModal: false,
+            confirmMessage: '',
+            pendingAction: null,
+            loadingIndicators: false,
+            loadingRecords: false,
+            localRecords: [],   // ← 新增
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF',
+            fileList: [], // 附件列表
+            uploadFileList: [], // 上传成功的fileList
+            isFileChange: false,
+            fileUploadVisible: false,
+            currentFile: null,
+            fileViewerDialogVisible: false,
+        };
+    },
+    watch: {
+        async showDrawer(val) {
+            this.showDrawerTow = val;
+            this.isFileChange = false
+            this.indicators = this.tableData;
+            this.setCurrentIndicator(this.indicators[this.currentIndex])
+            // this.getRecords(true)
+        },
+        
+        uploadFileList: {
+            handler(newValue, oldValue) {
+                this.isFileChange = true;
+            },
+            deep: true
+        }
+    },
+    methods: {
+
+        parseUrlFile(url) {
+            const [, fileName, ext] = url.match(/\/([^/?#]+)(\.\w+)(?:[?#]|$)/) || [];
+            return { fileName, ext };
+        },
+
+        onFilePreView2(fileUrl) {
+            this.currentFile = null;
+            let fileObj = this.parseUrlFile(fileUrl);
+            let type = fileObj.ext.replace(".", "")
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            if (imgFiles.includes(type)) {
+                this.imgUrl = ''
+                this.imgUrl = fileUrl;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                let currentFile = {
+                    name: fileObj.fileName + fileObj.ext,
+                    url: fileUrl,
+                    type
+                }
+                this.currentFile = currentFile;
+                this.fileViewerDialogVisible = true;
+            }
+        },
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+        // 选择指标
+        async selectIndicator(indicator) {
+
+            if (this.isFileChange || this.hasUnsavedChanges()) {
+                this.$confirm('请确认提交跟踪记录:' + this.currentRecord.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                }).then(async () => {
+                    await this.submitRecord();
+                    this.setCurrentIndicator(indicator);
+                }).catch(() => {
+                    this.resetForm();
+                    this.setCurrentIndicator(indicator);
+                })
+            } else {
+                this.setCurrentIndicator(indicator);
+            }
+            this.isFileChange = false
+        },
+
+        setCurrentIndicator(indicator) {
+            this.recordPage = 1;
+            this.currentIndicator = indicator;
+            this.currentRecord = null;
+            this.originalRecord = null;
+            this.getRecords(true);
+        },
+
+        
+        getRecords(is = false) {
+            if (!this.currentIndicator) return;
+            let reviewIndicatorId = this.currentIndicator.reviewIndicatorId
+            let url = `/performance/review/indicator/record/${this.user_info.site_id}/${reviewIndicatorId}`
+            let params = {
+                page: is ? 1 : this.recordPage,
+                pageSize: this.recordPageSize,
+            }
+            this.$axiosUser('get', url, params).then(res => {
+                this.records = res.data.data.list
+                this.recordTotal = res.data.data.total
+                if (this.records && this.records.length > 0) {
+                    this.setCurrentRecord(this.records[0])
+                }
+            })
+        },
+
+        handleRecordSizeChange(val) {
+            this.recordPage = 1;
+            this.recordPageSize = val;
+            this.getRecords()
+        },
+
+        handleRecordCurrentChange(val) {
+            this.recordPage = val;
+            this.recordPageSize = 10;
+            this.getRecords()
+        },
+
+        // 选择管理记录
+        selectRecord(record) {
+            if (this.isFileChange || this.hasUnsavedChanges()) {
+                this.$confirm('请确认提交跟踪记录:' + this.currentRecord.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                }).then(async () => {
+                    await this.submitRecord();
+                    this.setCurrentRecord(record);
+                }).catch(() => {
+                    this.resetForm();
+                    this.setCurrentRecord(record);
+                })
+            } else {
+                this.setCurrentRecord(record);
+            }
+            this.isFileChange = false
+        },
+
+        setCurrentRecord(record) {
+            this.currentRecord = JSON.parse(JSON.stringify(record));
+            this.originalRecord = JSON.parse(JSON.stringify(record));
+        },
+
+        deleteRecord() {
+            this.$confirm('确认删除跟踪记录:' + this.currentRecord.title + '?', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                let url = `/performance/review/indicator/record/remove/${this.user_info.site_id}/${this.currentRecord.reviewIndicatorRecordId}`
+                this.$axiosUser('post', url).then(res => {
+                    if (res.data.code == 1) {
+                        this.$message({ message: '删除成功', type: 'success' });
+                        this.recordPage = 1
+                        this.getRecords(true);
+                    }
+
+                });
+            }).catch(() => { });
+        },
+
+
+        // 新增记录
+        addRecord() {
+            // 如果当前有未保存更改,先处理
+            if (this.isFileChange || this.hasUnsavedChanges()) {
+                this.$confirm('请确认提交跟踪记录:' + this.currentRecord.title + '?', {
+                    confirmButtonText: '确定',
+                    cancelButtonText: '取消',
+                    type: 'warning'
+                }).then(async () => {
+                    await this.submitRecord();
+                    this.insertEmptyRecord();
+                }).catch(() => {
+                    this.resetForm();
+                    this.insertEmptyRecord();
+                })
+            } else {
+                this.insertEmptyRecord();
+            }
+            this.isFileChange = false
+        },
+
+        // 真正“插空记录”逻辑
+        insertEmptyRecord() {
+            const empty = {
+                employeeId: this.user_info.id,
+                reviewIndicatorRecordId: '',   // 后端返回真实主键
+                title: '',
+                content: '',
+                files: [],
+                createTime: '',
+            };
+            this.records.unshift(empty);     // ← 插在列表最前
+            this.setCurrentRecord(empty);    // 立即选中
+        },
+
+
+        // 重置表单
+        resetForm() {
+            if (this.originalRecord) {
+                this.currentRecord = JSON.parse(JSON.stringify(this.originalRecord));
+            }
+        },
+        
+
+        // 检查是否有未保存的更改
+        hasUnsavedChanges() {
+            if (!this.currentRecord || !this.originalRecord) return false;
+            return !deepEqual(this.currentRecord, this.originalRecord);
+        },
+
+
+        handleClose(done) {
+            this.currentIndicator = null;
+            this.currentRecord = null;
+            this.fileList = [];
+            this.uploadFileList = [];
+            this.isFileChange = false;
+            this.$emit('update:showDrawer', false);
+            this.$emit('confirm')
+        },
+
+
+        confirmAppendFile(files) {
+            this.uploadFileList = files
+            this.currentRecord.files = files
+            this.submitRecord()
+        },
+
+
+        async submitRecord() {
+            let data = {
+                title: this.currentRecord.title,
+                content: this.currentRecord.content,
+                files: this.currentRecord.files
+            };
+            let reg = /^[A-Za-z0-9-::,.!。!,;;、/_ ()()'"“‘\n\u4e00-\u9fa5]{0,50}$/
+            if (!this.currentRecord.title || !reg.test(this.currentRecord.title)) {
+                return this.$message.error("记录内容只能1-50位包含字母、数字、横线、下划线、小括号,必须以字母、数字、中文开头")
+            }
+            // 编辑
+            let reviewIndicatorRecordId = this.currentRecord.reviewIndicatorRecordId
+            if (reviewIndicatorRecordId) {
+                let url = `/performance/review/indicator/record/edit/${this.user_info.site_id}/${reviewIndicatorRecordId}`
+                this.$http.post(url, data).then(res => {
+                    if (res.code == 1) {
+                        let index = this.records.findIndex(record => record.reviewIndicatorRecordId == reviewIndicatorRecordId)
+                        this.records.splice(index, 1, res.data)
+                        // this.setCurrentRecord(res.data);
+                        this.$message.success('填写成功');
+                        // this.getRecords(true);
+                    } else {
+                        this.resetForm();
+                        this.$message.error(res.message || '填写失败');
+                    }
+                })
+            // 新增
+            } else {
+                let url = `/performance/review/indicator/record/create/${this.user_info.site_id}/${this.currentIndicator.reviewIndicatorId}`
+                this.$http.post(url, data).then(res => {
+                    if (res.code == 1) {
+                        // 替换第一个元素,新增的元素
+                        this.records.splice(0, 1, res.data)
+                        this.setCurrentRecord(res.data);
+                        this.$message.success('新增成功');
+                        this.getRecords(true);
+                    } else {
+                        this.resetForm();
+                        this.$message.error(res.message || '填写失败');
+                    }
+                })
+            }
+
+            this.isFileChange = false
+        }
+
+        
+    }
+};
+</script>
+
+<style scoped="scoped">
+::v-deep .el-icon-picture-outline {}
+
+.all-derawer ::v-deep.el-drawer {
+    height: 80% !important;
+    border-radius: 20px 20px 0 0;
+    background-color: #f5f7fa;
+    min-width: 1100px !important;
+}
+
+.all-derawer ::v-deep.el-drawer__body::-webkit-scrollbar {
+    display: none !important;
+}
+
+.all-derawer ::v-deep.el-drawer__header {
+    background-color: #f5f7fa;
+    padding: 12px 20px;
+    box-sizing: border-box;
+    margin-bottom: 0px;
+    font-size: 16px;
+}
+
+::v-deep .el-upload-list__item-name {
+    max-width: 250px;
+}
+
+::v-deep :focus {
+    outline: 0;
+}
+
+.btns {
+    position: absolute;
+    right: 20px;
+    top: 16px;
+    z-index: 999;
+}
+
+.plan-right-name {
+    color: #222;
+}
+
+.plan-right {
+    padding: 20px;
+    position: relative;
+    background-color: #f5f7fa;
+    min-height: 600px;
+}
+
+.plan-right-date {
+    text-align: center;
+    position: relative;
+    margin-bottom: 30px;
+}
+
+.plan-textarea ::v-deep textarea {
+    background-color: #f5f7fa;
+    border: none;
+}
+
+.plan-right-title {
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.plan-right-title ::v-deep textarea {
+    background-color: #f5f7fa;
+    border: none;
+}
+
+.drawer-left {
+    background-color: #fff;
+    padding: 0 15px;
+    width: 280px;
+    border-right: 1px solid #f1f1f1;
+    overflow: auto;
+    padding-bottom: 60px;
+}
+
+.drawer-left-title {
+    padding: 16px 0;
+    font-size: 16px;
+    color: #222;
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.drawer-item {
+    padding: 16px 0;
+    cursor: pointer;
+}
+
+.drawer-left-title.active {
+    font-weight: 600;
+}
+
+.drawer-left-title:hover, .drawer-item:hover {
+    background-color: #f5f7fa;
+}
+
+.drawer-left .active-drawer-item {
+    background-color: #f5f7fa;
+    border-right: 2px solid #409EFF;
+}
+
+.plan-left-content:hover {
+    background-color: #f5f7fa;
+}
+
+.plan-left .active-drawer-item {
+    background-color: #f5f7fa;
+}
+
+.drawer-right-header {
+    background-color: #fff;
+    border-bottom: 1px solid #f1f1f1;
+    padding: 20px;
+    box-sizing: border-box;
+}
+
+.drawer-right-main {
+    padding: 0px 20px;
+    height: 50px;
+    line-height: 50px;
+    background-color: #fff;
+    border-bottom: 1px solid #f1f1f1;
+}
+
+.plan-title {
+    font-size: 14px;
+    color: #666666;
+    margin-top: 5px;
+}
+
+.plan-left {
+    background-color: #fff;
+    border-right: 1px solid #f1f1f1;
+    /* overflow: auto; */
+}
+
+.remark {
+    height: 60px;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 6;
+    overflow: hidden;
+}
+
+.plan-left-content {
+    border-bottom: 1px solid #f1f1f1;
+    padding: 10px 16px;
+    cursor: pointer;
+    width: 100%;
+}
+
+
+.pagination-box {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 10px;
+    position: fixed;
+    bottom: 10px;
+    left: 50%;
+    background: #fff;
+    transform: translateX(-50%);
+}
+
+
+.file-item {
+    margin-bottom: 10px;
+}
+
+</style>

+ 95 - 11
src/newPerformance/components/ProcessTracking.vue

@@ -2,8 +2,8 @@
     <div class="record-container">
         <div class="main-content">
             <div class="search-box">
+                
                 <div class="flex-box-ce">
-
                     <el-select style="width: 120px;" v-model="params.cycleType" placeholder="请选择周期种类"
                         @change="changeCycleType">
                         <el-option v-for="item in cycleOptions" :key="item.id" :label="item.name" :value="item.id">
@@ -68,15 +68,16 @@
                         </el-option>
                     </el-select>
 
-                    <!-- <div class="dept_wdiv flex-box-ce" style="margin: 0 0 0 10px;">
-                        <div class="dept_inp" @click="show_dept_selector = true">
-                            <span v-if="deptVisibleName != ''">{{ deptVisibleName }}</span>
-                            <span v-else style="color: #b9b9b9;">选择部门</span>
-                        </div>
-                        <i class="el-icon-arrow-down"></i>
-                    </div> -->
 
-                    <el-select v-model="selectedDeptIds" clearable multiple placeholder="请选择部门"
+                    <!-- 部门管理者查看自己本部门的数据 -->
+                    <el-select v-if="$getRole(2)" v-model="selectedDeptIds" class="tags-select-input" collapse-tags multiple placeholder="请选择部门"
+                        style="width: 300px; margin-left: 10px;" @change="changeDeptName" >
+                        <el-option v-for="(item, index) in allDeptList" :key="index" :label="item.name"
+                            :value="item.id"></el-option>
+                    </el-select>
+
+
+                    <el-select v-else v-model="selectedDeptIds" class="tags-select-input" collapse-tags clearable multiple placeholder="请选择部门"
                         style="width: 300px; margin-left: 10px;" @change="changeDeptName">
                         <el-option v-for="(item, index) in deptList" :key="index" :label="item.name"
                             :value="item.id"></el-option>
@@ -197,12 +198,13 @@
 <script>
 let that;
 import { mapGetters } from 'vuex';
-import moment from 'moment';
 import TargetListComp from "@/performance/views/assessManagement/TargetListComp.vue"; // 关联OKR弹框
 import EmployeeSelector from '@/components/EmployeeSelector'; // 部门选择
 import MyPerformance from './MyPerformance'; // 我的考核
 import XLSX from 'xlsx';
 import FileSaver from 'file-saver';
+import { getPaths} from '@/utils/findDeptItem.js';
+import moment from 'moment';
 
 export default {
     components: {
@@ -279,6 +281,7 @@ export default {
             },
             dept_select_id: '',
             keyword: '',
+            allDeptList: [],
             deptList: [], // 部门列表 - 树形结构
             dept_list: [], // 部门列表
             employeeMap: this.$getEmployeeMap(), // 员工列表
@@ -314,6 +317,7 @@ export default {
             isFold: false,
             dateRange: [], 
             detailDialogVisible: false,
+            manageDeptIds: [], // 部门管理员管理的部门列表
             pickerOptions: {
                 shortcuts: [{
                     text: '最近一周',
@@ -347,7 +351,9 @@ export default {
     async created() {
         that = this;
         await this.getAllSet();
+        // 部门管理者请求自己所在部门,并回显
         this.initDefaultParams();
+        
     },
 
     beforeDestroy() {
@@ -389,6 +395,46 @@ export default {
 
     methods: {
 
+        
+
+        // 获取部门
+        get_dept_list() {
+            this.$axiosUser('get', '/api/pro/department/tree', '', 'v2').then(res => {
+                let list = res.data.data.list;
+                let allDeptList = this.getTreeData(list);
+                /* 部门管理者,回显管理的部门 */
+                const ids = this.selectedDeptIds; 
+                const tree = [...allDeptList]; // 部门树形结构数据
+                let pathItems = getPaths(ids, tree);
+                if (pathItems.length === 0) return;
+                let paths = []
+                pathItems.forEach(item => {
+                    if (item.path.length > 0) {
+                        item.path.forEach(dept => {
+                            paths.push(dept);
+                        });
+                    }
+                });
+                if(paths.length === 0) return
+                this.allDeptList = paths.filter(path => ids.includes(path.id))
+            });
+        },
+
+        // 处理部门树状结构数据
+        getTreeData(data) {
+            for (var i = 0; i < data.length; i++) {
+                data[i].checked = false;
+                if (data[i].children.length < 1) {
+                    // children若为空数组,则将children设为undefined
+                    data[i].children = undefined;
+                } else {
+                    // children若不为空数组,则继续 递归调用 本方法
+                    this.getTreeData(data[i].children);
+                }
+            }
+            return data;
+        },
+
         /* 开启 dialog 时:禁用返回键 */
         onDialogOpen() {
             // 立即往 history 里插一条空记录,拦截返回
@@ -453,6 +499,18 @@ export default {
 
             this.params.startDate = firstDayStr
             this.params.endDate = lastDayStr
+
+            // 部门管理者
+            if (this.$getRole(2)) {
+                let manageDeptIds = JSON.parse(localStorage.getItem("userInfo")).employee_detail.manage_dept_ids || []
+                this.selectedDeptIds = manageDeptIds.map(Number)
+                this.params.deptIds = this.selectedDeptIds.toString()
+                this.get_dept_list();
+            } else {
+                this.selectedDeptIds = []
+                this.params.deptIds = ''
+            }
+
             this.getRecords()
         },
 
@@ -541,13 +599,20 @@ export default {
         },
 
         changeDeptName(v) {
+            // 部门管理者,必须要有部门ids才能查询
+            if (this.$getRole(2)) {
+                if (v.length === 0) {
+                    let manageDeptIds = JSON.parse(localStorage.getItem("userInfo")).employee_detail.manage_dept_ids || []
+                    this.selectedDeptIds = manageDeptIds.map(Number)
+                } 
+            }
+            
             this.params.deptIds = this.selectedDeptIds.toString();
             this.getRecords();
         },
 
         // 员工筛选
         changeEmployeeIds(v) {
-            console.log(v)
             this.selectEmployeeId = v
             this.applyFilters();
         },
@@ -857,6 +922,25 @@ export default {
     }
 }
 
+.tags-select-input {
+    height: 40px;
+}
+
+.tags-select-input /deep/ .el-select__tags{
+    height: 40px;
+    white-space: nowrap;
+    overflow: hidden;
+    flex-wrap: nowrap;
+}
+
+
+.tags-select-input /deep/ .el-select__tags-text {
+    display: inline-block;
+    max-width: 85px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
 
 /* 默认鼠标样式为箭头指针 */
 .record-container {

+ 150 - 12
src/newPerformance/components/PublicComp/TargetPlanComp.vue

@@ -18,17 +18,127 @@
                     v-if="sourceData && sourceData.plans && sourceData.plans.length">
                     <template v-if="activeName === 'first'">
                         <vxe-table :data="sourceData.plans" border style="width: 100%;" height="auto">
-                            <vxe-column field="name" title="任务标题" show-overflow :width="300" />
-                            <vxe-column field="ownerName" title="负责人" width="100" />
-                            <vxe-column field="startTime" title="任务时间" width="200">
+                            <vxe-column field="name" title="任务标题" show-overflow min-width="300">
                                 <template #default="{ row }">
-                                    <div v-if="row.startTime && row.endTime" class="flex-box-ce">{{ `${row.startTime} 至
-                                        ${row.endTime}` }}</div>
+                                    <el-link type="primary" @click="openTaskDetail(row)">
+                                        {{ row.name }}
+                                    </el-link>
+
+                                    <el-popover ref="popoverRef" placement="right" trigger="hover">
+                                        <template #reference>
+                                            <el-tag type="primary">对齐</el-tag>
+                                        </template>
+
+                                        <div>
+                                            <template v-if="row.mainParent">
+                                                <div>
+                                                    <el-link type="info" style="margin-bottom: 10px;">
+                                                        所属KR/项目&nbsp;:&nbsp;
+                                                    </el-link>
+                                                    <div style="padding: 0; margin: 0 0 10px 0;">
+                                                        <div class="flex-box-ce">
+                                                            <el-tag size="mini"
+                                                                v-if="row.mainParent.processLast !== null"
+                                                                :type="row.mainParent.processLast > row.mainParent.processLast ? 'success' : (row.mainParent.process < row.mainParent.processLast ? 'danger' : 'warning')">
+                                                                <i v-if="row.mainParent.process > row.mainParent.processLast"
+                                                                    class="el-icon-top"></i>
+                                                                <i v-else-if="row.mainParent.process < row.mainParent.processLast"
+                                                                    class="el-icon-bottom"></i>
+                                                                {{ row.mainParent.process }}%
+                                                            </el-tag>
+                                                            <el-tag size="mini" type="primary" class="text-hidden"
+                                                                style="max-width: 300px; margin: 0 10px;">
+                                                                {{ row.mainParent.name }}
+                                                            </el-tag>
+                                                        </div>
+
+
+                                                        <template v-if="row.mainParent.children">
+                                                            <div class="flex-box-ce">
+                                                                <div style="width: 30px;"></div>
+                                                                <div style="margin: 0 5px;">-</div>
+                                                                <el-tag size="mini"
+                                                                    v-if="row.mainParent.children.processLast !== null"
+                                                                    :type="row.mainParent.children.processLast > row.mainParent.children.processLast ? 'success' : (row.mainParent.children.process < row.mainParent.children.processLast ? 'danger' : 'warning')">
+                                                                    <i v-if="row.mainParent.children.process > row.mainParent.children.processLast"
+                                                                        class="el-icon-top"></i>
+                                                                    <i v-else-if="row.mainParent.children.process < row.mainParent.children.processLast"
+                                                                        class="el-icon-bottom"></i>
+                                                                    {{ row.mainParent.children.process }}%
+                                                                </el-tag>
+                                                                <el-tag size="mini" type="success" class="text-hidden"
+                                                                    style="max-width: 300px; margin: 0 10px;">
+                                                                    {{ row.mainParent.children.name }}
+                                                                </el-tag>
+                                                            </div>
+                                                        </template>
+                                                    </div>
+
+                                                </div>
+                                            </template>
+
+                                            <template v-if="row.otherParent && row.otherParent.length > 0">
+                                                <el-link type="info" style="margin-bottom: 10px;">
+                                                    关联其他&nbsp;:&nbsp;
+                                                </el-link>
+                                                <div v-for="parent in row.otherParent" :key="parent.id"
+                                                    style="padding: 0; margin: 0 0 10px 0;">
+                                                    <div class="flex-box-ce">
+                                                        <el-tag size="mini" v-if="parent.processLast !== null"
+                                                            :type="parent.processLast > parent.processLast ? 'success' : (parent.process < parent.processLast ? 'danger' : 'warning')">
+                                                            <i v-if="parent.process > parent.processLast"
+                                                                class="el-icon-top"></i>
+                                                            <i v-else-if="parent.process < parent.processLast"
+                                                                class="el-icon-bottom"></i>
+                                                            {{ parent.process }}%
+                                                        </el-tag>
+                                                        <el-tag size="mini" type="info" class="text-hidden"
+                                                            style="max-width: 300px; margin: 0 10px;">
+                                                            {{ parent.name }}
+                                                        </el-tag>
+                                                    </div>
+
+                                                    <br />
+                                                    <template v-if="parent.children">
+                                                        <div class="flex-box-ce">
+                                                            <div style="width: 30px;"></div>
+                                                            <div style="margin: 0 5px;">-</div>
+                                                            <el-tag size="mini"
+                                                                v-if="parent.children.processLast !== null"
+                                                                :type="parent.children.processLast > parent.children.processLast ? 'success' : (parent.children.process < parent.children.processLast ? 'danger' : 'warning')">
+                                                                <i v-if="parent.children.process > parent.children.processLast"
+                                                                    class="el-icon-top"></i>
+                                                                <i v-else-if="parent.children.process < parent.children.processLast"
+                                                                    class="el-icon-bottom"></i>
+                                                                {{ parent.children.process }}%
+                                                            </el-tag>
+                                                            <el-tag size="mini" type="info" class="text-hidden"
+                                                                style="max-width: 300px; margin: 0 10px;">
+                                                                {{ parent.children.name }}
+                                                            </el-tag>
+                                                        </div>
+                                                    </template>
+                                                </div>
+                                            </template>
+
+                                            <div class="fontColorC"
+                                                v-if="!row.mainParent && !(row.otherParent && row.otherParent.length > 0)">
+                                                暂无对齐</div>
+                                        </div>
+                                    </el-popover>
+                                </template>
+                            </vxe-column>
+
+                            
+                            <vxe-column field="ownerName" title="负责人" min-width="100" />
+                            <vxe-column field="startTime" title="任务时间" min-width="200">
+                                <template #default="{ row }">
+                                    <div v-if="row.startTime && row.endTime" class="flex-box-ce">{{ `${row.startTime} 至 ${row.endTime}` }}</div>
                                     <span v-else>--</span>
                                 </template>
                             </vxe-column>
 
-                            <vxe-column field="process" title="进度" width="300">
+                            <vxe-column field="process" title="进度" min-width="300">
                                 <template #default="{ row }">
                                     <div class="flex-box-ce">
                                         <el-tag size="mini" v-if="row.processLast !== null"
@@ -50,7 +160,7 @@
                             </vxe-column>
 
 
-                            <vxe-column field="parent" title="对齐">
+                            <!-- <vxe-column field="parent" title="对齐">
                                 <template #default="{ row }">
                                     <template v-if="row.mainParent">
                                         <div>
@@ -128,7 +238,7 @@
                                         </div>
                                     </template>
                                 </template>
-                            </vxe-column>
+                            </vxe-column> -->
                         </vxe-table>
 
 
@@ -137,7 +247,8 @@
                     <template v-if="activeName === 'second'">
 
                         <vxe-table border :data="processHistory" :column-config="{ resizable: true }"
-                            :row-config="{ isHover: true, isCurrent: true }" height="auto" resizable show-overflow auto-resize>
+                            :row-config="{ isHover: true, isCurrent: true }" height="auto" resizable show-overflow
+                            auto-resize>
                             <vxe-column field="name" title="任务标题" show-overflow width="300" fixed="left" />
                             <vxe-column field="ownerName" title="负责人" width="100" fixed="left" />
                             <vxe-column field="process" title="最新进度" width="300">
@@ -167,7 +278,7 @@
                                         '')].process} %` }}</div>
                                 </template>
                             </vxe-column>
-                            
+
                         </vxe-table>
                     </template>
 
@@ -185,7 +296,13 @@
 
 
             </div>
+
+            <TaskDetail v-if="isShowTaskDetail2" :visible.sync="isShowTaskDetail" :readonly="false"
+                :taskId="selectTaskItem.id" @closeDialog="closeDialog" @confirm="closeDetail"></TaskDetail>
         </div>
+
+
+
     </el-dialog>
 </template>
 
@@ -193,7 +310,7 @@
 <script>
 import { mapGetters } from 'vuex';
 import moment from "moment";
-
+import TaskDetail from '@/okr/components/public/TaskDetail'; //任务内容
 
 const compositeStates = {
     1:{ title: '未开始', color: 'info' },
@@ -211,6 +328,9 @@ const compositeStates = {
 
 export default {
     name: "TargetPlanComp",
+    components: {
+        TaskDetail
+    },
     data() {
         return {
             activeName: "first",
@@ -231,6 +351,9 @@ export default {
             },
             myChart1: null,
             myChart2: null,
+            selectTaskItem: null,
+            isShowTaskDetail: false,
+            isShowTaskDetail2: false
         }
     },
 
@@ -250,7 +373,6 @@ export default {
     watch: {
         activeName(newVal) {
             if (newVal === 'third') {
-                console.log('切换到显示图表');
                 this.renderChart();
             }
         }
@@ -506,6 +628,22 @@ export default {
             })
         },
 
+
+        openTaskDetail(row) {
+            console.log(row)
+            this.selectTaskItem = row;
+            this.isShowTaskDetail2 = true;
+            this.$nextTick(() => {
+                this.isShowTaskDetail = true;
+            })
+        },
+
+        closeDialog() {
+
+        },
+        closeDetail() {
+
+        }
     }
 }
 </script>

+ 2 - 0
src/newPerformance/components/PublicComp/WorkFlow/index.vue

@@ -488,6 +488,8 @@ export default {
             return uuid;
         },
 
+        
+
         // 获取部门
         get_dept_list() {
             return this.$axiosUser('get', '/api/pro/department/tree', '', 'v2')

+ 117 - 45
src/newPerformance/components/Workbench/EditNode.vue

@@ -5,7 +5,12 @@
             <div class="status-btn-box fadeInDown animated">
                 <el-link type="primary" @click="detailsDialogVisible = true">处理详情</el-link>
                 <el-link type="primary" @click="examineLogDialogVisible = true">操作日志</el-link>
-                <el-link v-if="allowEditOkrs" type="primary" @click="showIndicatorTargetSearch = true">过程跟踪</el-link>
+
+                <el-link v-if="allowManageRecord" type="primary"
+                    @click="openTargetList(dialogData.reviewIndicatorId)">过程跟踪</el-link>
+                <el-link v-if="allowManageRecord" type="primary"
+                    @click="showManageRecord = true">管理记录</el-link>
+                <el-link v-if="allowEditOkrs" type="primary" @click="showIndicatorTargetSearch = true">绑定OKR</el-link>
             </div>
 
             <div class="dialog-content" v-loading="loading">
@@ -167,7 +172,8 @@
                 </div>
 
                 <!-- 录入信息 -->
-                <div class="dialog-content-right">
+                <div class="dialog-content-right"
+                    style="padding-bottom: 50px;">
 
                     <div class="data-box" v-if="activeName == 1">
                         <el-input v-model="formData && formData.title" placeholder="指标标题" style="width: 300px;"
@@ -267,26 +273,10 @@
                         <!-- <div slot="tip" class="el-upload__tip">(支持上传xlsx,xls,doc,docx,pdf,txt,png,jpeg,jpg,gif, 大小不能超过5M)</div> -->
                     </uploadOss>
 
-                    <!-- <div class="file-list"
-                        v-if="activeName > 1 && activeName <= 6 && hasUploadFiles.length > 0">
-                        <div class="title">已上传附件</div>
-                        <ul>
-                            <li v-for="(file, index) in uploadFileList" :key="index">
-                                
-                                <el-link type="primary" @click="onFilePreView2(file)">
-                                    附件{{ index + 1 }}
-                                </el-link>
-
-                                <el-link type="danger" @click="deleteHasUploadFile(index)">
-                                    删除
-                                </el-link>
-                            </li>
-                        </ul>
-                    </div> -->
-
-                    <!-- <el-button v-if="activeName > 1 && activeName <= 6" class="primaryBtn" icon="el-icon-check"
-                        type="primary" size="mini" @click="comfirmUploadFiles()"
-                        style="margin-top: 10px;">提交附件</el-button> -->
+                    <!-- 管理记录 -->
+                    <!-- <ShowManageRecords v-if="(isCreator || getRole1) && activeName == 5" :title="dialogData.title"
+                        :reviewIndicatorId="dialogData.reviewIndicatorId" /> -->
+
                 </div>
                 <!-- 录入信息 -->
             </div>
@@ -344,6 +334,18 @@
         <TargetSearch :visible.sync="showIndicatorTargetSearch" :selectedOkrs="indicatorOkrs"
             :selectedPlans="indicatorPlans" :key="`indicatorTarget`" @confirm="onIndicatorOkrSelected" />
 
+        <!-- 管理记录 -->
+        <ManageRecord :showDrawer.sync="showManageRecord"
+            :tableData="[{ title: dialogData.title, reviewIndicatorId: dialogData.reviewIndicatorId }]"
+            :currentIndex="0" @confirm="recordConfirm">
+        </ManageRecord>
+
+
+        <!-- 关联OKR -->
+        <TargetPlanComp v-if="targetDialogVisible" :dialogVisible="targetDialogVisible"
+            :reviewIndicatorId="reviewIndicatorId" @close="closeTargetList">
+        </TargetPlanComp>
+
     </div>
 
 </template>
@@ -361,8 +363,9 @@ import EmployeeSelector from '@/components/EmployeeSelector'; // 员工选择组
 import ExamineLogDialog from '@/newPerformance/components/MyPerformance/ExamineLog'; // 考核日志
 import Vcode from "vue-puzzle-vcode";
 import TargetSearch from "@/performance/views/assessManagement/TargetSearch";
-
-
+import ShowManageRecords from "./ShowManageRecords.vue";
+import ManageRecord from '../MyPerformance/ManageRecord.vue';
+import TargetPlanComp from "@/newPerformance/components/PublicComp/TargetPlanComp.vue"; // okr列表
 export default {
     components: {
         DetailsDialog,
@@ -371,6 +374,9 @@ export default {
         ExamineLogDialog,
         Vcode,
         TargetSearch,
+        ShowManageRecords,
+        ManageRecord,
+        TargetPlanComp
     },
 
     model: {
@@ -428,6 +434,7 @@ export default {
             else return "--"
         }
     },
+
     data() {
         return {
             examineLogDialogVisible: false,
@@ -455,6 +462,8 @@ export default {
             hasUploadFiles: [],
             isUploadFile: true,
             showEmployeeSelector: false,
+            showManageRecord: false,
+            targetDialogVisible: false,
             employeeSelectedObj: {
                 employee: [],
                 dept: []
@@ -549,11 +558,18 @@ export default {
             return this.activeName == 1 && (this.isCreator || this.getRole1 || isSelf)
         },
 
+        // 允许编辑管理记录,允许查看过程跟踪
+        allowManageRecord() {
+            let assignee = this.dialogData && this.dialogData.task && this.dialogData.task.assignee
+            return (this.isCreator || this.getRole1 || assignee == this.user_info.id) && this.activeName == 5
+        },
+
 
         // 整个审批的状态 0,进行中 1,已完成
         reviewStatus() {
             return this.dialogData && this.dialogData.reviewStatus
         },
+
         // 自评节点
         scoreSelf() {
             // 通过解构赋予默认值
@@ -562,6 +578,7 @@ export default {
                 return this.dialogData.flow.nodes.find(node => node.type == 'scoreSelf')
             }
         },
+
         // 互评节点
         scoreEachOther() {
             // 通过解构赋予默认值
@@ -601,21 +618,71 @@ export default {
 
     methods: {
 
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+        closeTargetList() {
+            this.targetDialogVisible = false
+        },
+
+        openTargetList(reviewIndicatorId) {
+            this.reviewIndicatorId = reviewIndicatorId;
+            this.targetDialogVisible = true
+            this.onDialogOpen();
+        },
+
+        recordConfirm() {
+            this.showManageRecord = false;
+        },
+        getRecords(is = false) {
+            if (!this.currentIndicator) return;
+            let reviewIndicatorId = this.currentIndicator.reviewIndicatorId
+            let url = `/performance/review/indicator/record/${this.user_info.site_id}/${reviewIndicatorId}`
+            let params = {
+                page: is ? 1 : this.recordPage,
+                pageSize: this.recordPageSize,
+            }
+            this.$axiosUser('get', url, params).then(res => {
+                this.records = res.data.data.list
+                this.recordTotal = res.data.data.total
+                if (this.records && this.records.length > 0) {
+                    this.setCurrentRecord(this.records[0])
+                }
+            })
+        },
+
+        parseUrlFile(url) {
+            const [, fileName, ext] = url.match(/\/([^/?#]+)(\.\w+)(?:[?#]|$)/) || [];
+            return { fileName, ext };
+        },
 
         /* 主函数:接收 url 数组 → 批量回显 */
         async batchLoad() {
             const urlArr = this.dialogData.task && this.dialogData.task.files ? this.dialogData.task.files : []; // 已成功上传的文件列表
             if (!urlArr.length) return
-
-            /* 并发下载并转成 File */
-            const taskList = urlArr.map(u => this.url2File(u))
-            try {
-                const files = await Promise.all(taskList)
-                this.fileList = files                // 一次性塞给 el-upload
-                // this.$message.success(`已回显 ${files.length} 个文件`)
-            } catch (e) {
-                this.$message.error('下载失败:' + e.message)
-            }
+            this.fileList = []  // 清空,准备塞入
+            this.loading = true
+            this.fileList = urlArr.map(u => ({
+                name: this.parseUrlFile(u).fileName + this.parseUrlFile(u).ext,
+                url: u,
+                status: 'success'
+            }))
+            this.loading = false
+
+            // /* 并发下载并转成 File */
+            // const taskList = urlArr.map(u => this.url2File(u))
+            // try {
+            //     const files = await Promise.all(taskList)
+            //     this.fileList = files                // 一次性塞给 el-upload
+            //     // this.$message.success(`已回显 ${files.length} 个文件`)
+            // } catch (e) {
+            //     this.$message.error('下载失败:' + e.message)
+            // }
         },
 
         /* 单条 url → File → el-upload 对象 */
@@ -736,32 +803,39 @@ export default {
         },
 
         handleInputResult(value) {
-            // 使用正则表达式限制输入
-            const regex = /^([0-9])*$/; // 匹配非负整
+
+            const regex = /^-?$|^(-?\d+\.?\d*)$/; // 允许空、负号、数字/小
 
             // 如果输入值不符合正则表达式,则恢复为之前的值
             if (!regex.test(value)) {
                 value = ''; // 重置输入框的值
                 this.formData.result = ''
             } else {
-                // 如果输入值符合正则表达式,更新绑定的值
                 this.formData.result = Number(value);
             }
         },
 
         handleInputScore(value) {
+            // console.log(value)
             // 使用正则表达式限制输入
-            const regex = /^([0-9])*$/; // 匹配非负整
+            const regex = /^-?$|^(-?\d+\.?\d*)$/; // 允许空、负号、数字/小
 
             // 如果输入值不符合正则表达式,则恢复为之前的值
             if (!regex.test(value)) {
                 value = ''; // 重置输入框的值
-                if (this.formData && this.formData.task && this.formData.task.score)
-                    this.formData.task.score = 0
+                this.formData.task.score = ''
+                // if (this.formData && this.formData.task && this.formData.task.score)
+                //     this.formData.task.score = 0
+                
             } else {
-                // 如果输入值符合正则表达式,更新绑定的值
-                if (this.formData && this.formData.task && this.formData.task.score)
-                    this.formData.task.score = Number(value);
+                if (value.startsWith("-")) {
+                    this.formData.task.score = value;
+                } else {
+                    // 如果输入值符合正则表达式,更新绑定的值
+                    if (this.formData && this.formData.task && this.formData.task.score)
+                        this.formData.task.score = Number(value);
+                }
+                
             }
         },
 
@@ -950,8 +1024,6 @@ export default {
         },
 
         
-
-        
         handleSuccess: _debounce(function (response, file, fileList) {
             this.uploadFileList = this.fileList.map(item => {
                 return item.url;

+ 152 - 0
src/newPerformance/components/Workbench/ShowManageRecords.vue

@@ -0,0 +1,152 @@
+<template>
+    <div>
+        <div class="flex-box-ce" style="justify-content: space-between; margin: 10px 0;">
+            <div><strong>管理记录 ({{ recordTotal }}) </strong></div>
+            <div><el-link type="primary" @click="showManageRecord = true">查看更多</el-link></div>
+        </div>
+        <template v-if="recordTotal > 0">
+            <div v-for="(record, index) in records.slice(0, 5)" :key="index"
+                style="margin-bottom: 10px; padding: 10px; border-bottom: 1px solid #f1f1f1;">
+                <div class="flex-box-ce" style="justify-content: space-between;">
+                    <div><strong>{{ $getEmployeeMapItem(record.employeeId).name }}</strong></div>
+                    <div>{{ record.createTime }}</div>
+                </div>
+                <div>标题: {{ record.title }}</div>
+                <div>内容: {{ record.content }}
+                </div>
+
+                <div class="file-item" v-for="(file, index) in record.files" :key="file">
+                    <el-link type="primary" @click="onFilePreView2(file)">
+                        <i class="el-icon-document"></i>
+                        附件{{ (index + 1) }}
+                    </el-link>
+                </div>
+            </div>
+        </template>
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <PreviewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
+        <!-- 管理记录 -->
+        <ManageRecord :showDrawer.sync="showManageRecord" :tableData="[{ title, reviewIndicatorId }]"
+            :currentIndex="0" @confirm="recordConfirm">
+        </ManageRecord>
+    </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import PreviewFile from '@/okr/views/targetBusiness/components/previewFile.vue';
+import ManageRecord from '../MyPerformance/ManageRecord.vue';
+
+export default {
+    name: 'ShowManageRecords',
+    components: {
+        PreviewFile,
+        ManageRecord
+    },
+    props: {
+        title: {
+            type: String,
+            default: '',
+            required: true
+        },
+        reviewIndicatorId: {
+            type: Number | String,
+            required: true
+        }
+    },
+    computed: {
+        ...mapGetters(['user_info']),
+    },
+    data() {
+        return {
+            recordPage: 1,
+            recordPageSize: 10,
+            records: [],
+            recordTotal: 0,
+            currentFile: null,
+            fileViewerDialogVisible: false,
+            showManageRecord: false
+        };
+    },
+
+    watch: {
+        reviewIndicatorId(newVal, oldVal) {
+            this.getRecords();
+        }
+    },
+    
+    methods: {
+        getRecords() {
+            if (!this.reviewIndicatorId) return;
+            let reviewIndicatorId = this.reviewIndicatorId
+            let url = `/performance/review/indicator/record/${this.user_info.site_id}/${reviewIndicatorId}`
+            let params = {
+                page: this.recordPage,
+                pageSize: this.recordPageSize,
+            }
+            this.$axiosUser('get', url, params).then(res => {
+                this.records = res.data.data.list
+                this.recordTotal = res.data.data.total
+            })
+        },
+
+        parseUrlFile(url) {
+            const [, fileName, ext] = url.match(/\/([^/?#]+)(\.\w+)(?:[?#]|$)/) || [];
+            return { fileName, ext };
+        },
+
+        onFilePreView2(fileUrl) {
+            this.currentFile = null;
+            let fileObj = this.parseUrlFile(fileUrl);
+            let type = fileObj.ext.replace(".", "")
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            if (imgFiles.includes(type)) {
+                this.imgUrl = ''
+                this.imgUrl = fileUrl;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                let currentFile = {
+                    name: fileObj.fileName + fileObj.ext,
+                    url: fileUrl,
+                    type
+                }
+                this.currentFile = currentFile;
+                this.fileViewerDialogVisible = true;
+            }
+        },
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+        recordConfirm() {
+            this.getRecords();
+        }
+    }
+};
+</script>

+ 0 - 2
src/newPerformance/views/Index.vue

@@ -285,8 +285,6 @@ export default {
                 this.entranceList = menuList
             }
 
-            console.log("this.entranceList")
-            console.log(this.entranceList)
         },
 
         startGuide() {

+ 3 - 1
src/newPerformance/views/MoreNode.vue

@@ -665,6 +665,7 @@ export default {
 
         async handleCommentEdit(row, type, index) {
             this.selectRow = row
+            console.log(this.selectRow)
             if (this.loading) return
             let { reviewIndicatorId } = row;
             let { taskId, nodeType } = row.task
@@ -704,7 +705,7 @@ export default {
 
             let data = {
                 taskId,
-                comment: row.comment,
+                comment: row.task.comment,
                 complete: type == 1 ? false : true // false 暂存,true 提交
             }
 
@@ -715,6 +716,7 @@ export default {
             //     }
 
             // }
+            
             this.$http.post(url, data).then(res => {
                 // 暂存意见
                 if (type == 1) {

+ 7 - 8
src/okr/views/okrIndex.vue

@@ -78,12 +78,12 @@ export default {
       keepAliveView2: ['myProject', 'deptProject', 'publicProject', 'allProject'],  //需要缓存的组件名称列表,用逗号分隔
     };
   },
-  
+
+
   watch: {
     $route(to, form) {
       let targetBusinessRoutes = ['companyTargetList', 'deptTargetList', 'personalTargetList', 'cycleList', 'dataStatistics']
       if (targetBusinessRoutes.includes(to.name)) {
-        console.log("经营目标相关页面");
         if (!localStorage.getItem("isTargetLearned"))
           this.dialogVisible = true;
       }
@@ -113,6 +113,7 @@ export default {
       });
     }
   },
+
   computed: {
     // 使用对象展开运算符将 getter 混入 computed 对象中
     ...mapGetters(['sumTotal', 'site_info']),
@@ -132,6 +133,8 @@ export default {
       this.$store.dispatch('getSumTotal'); //通知数量
     }
   },
+
+
   methods: {
 
 
@@ -153,6 +156,8 @@ export default {
       })
     },
 
+
+
     initRouter() {
       let dept_manager = this.$getIsIdentity('dept_manager'); //是否部门管理员
       let is_okr_manager = this.userInfo.is_okr_manager;//是否目标管理员
@@ -203,12 +208,8 @@ export default {
         })
       }
 
-      
       this.routers = routers;
 
-      console.log(this.routers)
-
-
       this.$nextTick(() => {
         let okr_path = this.$getCache('okr_path');
         if (okr_path) {
@@ -220,7 +221,6 @@ export default {
       });
     },
 
-    
 
     returnRoutersArr(str) {
       let routers = [];
@@ -273,7 +273,6 @@ export default {
       }
     },
 
-
     handleClose() {
       localStorage.setItem("isTargetLearned", true)
       this.dialogVisible = false;

+ 1656 - 0
src/okr/views/targetBusiness/companyTargetList copy.vue

@@ -0,0 +1,1656 @@
+<template>
+    <div id="app">
+        <div class="container">
+            <header>
+                <h1>Vue2 文件预览器</h1>
+                <div class="subtitle">支持PDF、Excel、图片和Word文档预览</div>
+            </header>
+
+            <div class="main-content">
+                <div class="sidebar">
+                    <div class="upload-section">
+                        <input type="file" id="fileInput" ref="fileInput" @change="handleFileUpload"
+                            accept=".pdf,.xlsx,.xls,.jpg,.jpeg,.png,.docx" style="display: none">
+                        <button class="upload-btn" @click="triggerFileInput">
+                            <span class="upload-icon">📤</span>
+                            上传文件
+                        </button>
+                    </div>
+
+                    <ul class="file-list">
+                        <li v-for="file in files" :key="file.id" class="file-item"
+                            :class="{ active: currentFile.id === file.id }" @click="selectFile(file)">
+                            <span class="file-icon">{{ getFileIcon(file.type) }}</span>
+                            <div class="file-info">
+                                <div class="file-name">{{ file.name }}</div>
+                                <div class="file-size">{{ formatFileSize(file.size) }}</div>
+                            </div>
+                            <button class="remove-btn" @click.stop="removeFile(file.id)">×</button>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="preview-area">
+                    <div class="preview-header">
+                        <h3>{{ currentFile.name || '文件预览' }}</h3>
+                        <div class="file-actions" v-if="currentFile.id">
+                            <button class="action-btn" @click="downloadFile">
+                                <span class="action-icon">⬇️</span>
+                                下载
+                            </button>
+                        </div>
+                    </div>
+
+                    <div class="preview-container">
+                        <div v-if="!currentFile.id" class="preview-content">
+                            <div class="preview-placeholder">
+                                <div class="placeholder-icon">📄</div>
+                                <h3>选择文件进行预览</h3>
+                                <p>支持 PDF、Excel、图片和 Word 文档格式</p>
+                                <button class="upload-here-btn" @click="triggerFileInput">
+                                    点击上传文件
+                                </button>
+                            </div>
+                        </div>
+
+                        <div v-else class="preview-content">
+                            <!-- 加载状态 -->
+                            <div v-if="loading" class="loading">
+                                <div class="spinner"></div>
+                                <p>加载中...</p>
+                                <p v-if="loadProgress > 0">加载进度: {{ loadProgress }}%</p>
+                            </div>
+
+                            <!-- PDF预览 -->
+                            <div v-else-if="currentFile.type === 'pdf'" class="pdf-preview">
+                                <div class="error-message" v-if="pdfError">
+                                    <h3>PDF加载失败</h3>
+                                    <p>{{ pdfError }}</p>
+                                    <button @click="retryLoadPdf"
+                                        style="margin-top: 10px; padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;">
+                                        重试
+                                    </button>
+                                </div>
+
+                                <div v-else>
+                                    <div class="pdf-controls">
+                                        <button @click="prevPage" :disabled="pdfPageNum <= 1">
+                                            <span>◀</span> 上一页
+                                        </button>
+                                        <span class="page-info">
+                                            第 {{ pdfPageNum }} 页 / 共 {{ pdfTotalPages }} 页
+                                        </span>
+                                        <button @click="nextPage" :disabled="pdfPageNum >= pdfTotalPages">
+                                            下一页 <span>▶</span>
+                                        </button>
+                                        <select v-model="pdfScale" @change="handleScaleChange" class="scale-select">
+                                            <option value="0.5">50%</option>
+                                            <option value="0.75">75%</option>
+                                            <option value="1">100%</option>
+                                            <option value="1.25">125%</option>
+                                            <option value="1.5">150%</option>
+                                            <option value="2">200%</option>
+                                        </select>
+                                        <button @click="fitToWidth">适应宽度</button>
+                                    </div>
+                                    <div class="pdf-viewer">
+                                        <canvas ref="pdfCanvas"></canvas>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <!-- 图片预览 -->
+                            <div v-else-if="currentFile.type === 'image'" class="image-preview-container">
+                                <img :src="currentFile.url" :alt="currentFile.name" class="image-preview"
+                                    :style="{ transform: `scale(${imageScale})` }">
+                                <div class="image-actions">
+                                    <button @click="zoomIn" :disabled="imageScale >= 3">放大</button>
+                                    <button @click="zoomOut" :disabled="imageScale <= 0.5">缩小</button>
+                                    <button @click="resetZoom">重置</button>
+                                    <span class="zoom-level">{{ Math.round(imageScale * 100) }}%</span>
+                                </div>
+                            </div>
+
+
+
+                            <!-- Excel预览 -->
+                            <div v-else-if="currentFile.type === 'excel'" class="excel-preview">
+                                <!-- Sheet 切换 -->
+                                <div class="sheet-tabs" v-if="sheets.length">
+                                    <div class="tabs-container">
+                                        <div v-for="s in sheets" :key="s.name"
+                                            :class="['tab-item', { active: pickedSheet === s.name }]"
+                                            @click="changeSheet(s.name)">
+                                            <span class="tab-name">{{ s.name }}</span>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div v-if="tableData.length" class="excel-panel">
+                                    <!-- 表格信息栏 -->
+                                    <div class="excel-info">
+                                        <div class="info-item">
+                                            <span class="label">当前工作表:</span>
+                                            <span class="value">{{ pickedSheet }}</span>
+                                        </div>
+                                        <div class="info-item">
+                                            <span class="label">数据行数:</span>
+                                            <span class="value">{{ tableData.length }}</span>
+                                        </div>
+                                        <div class="info-item">
+                                            <span class="label">列数:</span>
+                                            <span class="value">{{ tableData[0] ? tableData[0].length : 0 }}</span>
+                                        </div>
+                                    </div>
+
+                                    <!-- 表格容器 -->
+                                    <div class="table-container">
+                                        <!-- 顶部横向滚动条 -->
+                                        <div class="horizontal-scrollbar" ref="horizontalScrollbar">
+                                            <div class="scrollbar-track" ref="scrollbarTrack"></div>
+                                        </div>
+
+                                        <!-- 表格区域 -->
+                                        <div class="table-wrapper" ref="tableWrapper">
+                                            <table class="excel-table" ref="excelTable">
+                                                <tbody>
+                                                    <tr v-for="(row, r) in tableData" :key="r"
+                                                        :class="{ 'header-row': r === 0 }">
+                                                        <td v-for="(cell, c) in row" :key="c"
+                                                            :rowspan="getRowspan(r, c)" :colspan="getColspan(r, c)"
+                                                            v-if="shouldShowCell(r, c)" :class="{
+                                                                'header-cell': r === 0,
+                                                                'data-cell': r > 0
+                                                            }">
+                                                            <div class="cell-content">
+                                                                {{ cell || '' }}
+                                                            </div>
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- 空状态 -->
+                                <div v-else class="empty-state">
+                                    <div class="empty-icon">📊</div>
+                                    <h3>暂无数据</h3>
+                                    <p>当前工作表没有数据</p>
+                                </div>
+                            </div>
+
+                            <!-- Word预览 -->
+                            <div v-else-if="currentFile.type === 'word'" class="word-preview">
+                                <div class="word-content" v-if="wordContent" v-html="wordContent"></div>
+                                <div v-else class="no-content">
+                                    无法预览此Word文档
+                                </div>
+                            </div>
+
+                            <!-- 不支持的文件类型 -->
+                            <div v-else class="unsupported-file">
+                                <div class="unsupported-icon">❌</div>
+                                <h3>不支持的文件格式</h3>
+                                <p>当前文件格式无法预览,请下载后查看</p>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <footer>
+                Vue2 文件预览示例
+            </footer>
+        </div>
+    </div>
+</template>
+
+<script>
+import * as XLSX from 'xlsx'
+import ExcelPreview from "@/components/ExcelPreview";
+import PdfPreview from "@/components/PdfPreview";
+// import pdfFileUrl from "@/assets/123.pdf"
+// import { previewIndicatorFile } from "@/api/dataFilling/indicatorDisposal";
+// 设置PDF.js worker路径 - 使用稳定版本的Worker
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
+
+export default {
+    name: "IndicationPreview",
+    data() {
+        return {
+            currentFile: {},
+            files: [],
+            fileIdCounter: 5,
+            loading: false,
+            loadProgress: 0,
+            pdfjsVersion: pdfjsLib.version,
+
+            // PDF相关
+            pdfPageNum: 1,
+            pdfTotalPages: 0,
+            pdfDoc: null,
+            pdfScale: 1.5,
+            pdfError: '',
+            scaleMode: 'scale', // 'scale' 或 'width'
+
+            // 图片相关
+            imageScale: 1,
+
+            // Excel相关
+            workbook: null,
+            sheets: [],
+            pickedSheet: '',
+            tableData: [],
+            mergeMap: {},
+            originalMerges: [], // 存储原始合并信息
+
+            // Word相关
+            wordContent: '',
+        };
+    },
+
+    methods: {
+        // 获取文件图标
+        getFileIcon(type) {
+            const icons = {
+                'pdf': '📕',
+                'image': '🖼️',
+                'excel': '📊',
+                'word': '📝'
+            };
+            return icons[type] || '📄';
+        },
+
+        // 格式化文件大小
+        formatFileSize(bytes) {
+            if (bytes === 0) return '0 B';
+            const k = 1024;
+            const sizes = ['B', 'KB', 'MB', 'GB'];
+            const i = Math.floor(Math.log(bytes) / Math.log(k));
+            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+        },
+
+        // 触发文件输入
+        triggerFileInput() {
+            this.$refs.fileInput.click();
+        },
+
+        // 处理文件上传
+        handleFileUpload(event) {
+            const file = event.target.files[0];
+            if (!file) return;
+
+            // 确定文件类型
+            const fileType = this.getFileType(file);
+
+            // 创建文件对象
+            const fileObj = {
+                id: this.fileIdCounter++,
+                name: file.name,
+                type: fileType,
+                file: file,
+                url: URL.createObjectURL(file),
+                size: file.size
+            };
+
+            this.files.push(fileObj);
+            this.selectFile(fileObj);
+
+            // 重置文件输入
+            event.target.value = '';
+        },
+
+        // 根据文件扩展名确定类型
+        getFileType(file) {
+            const name = file.name.toLowerCase();
+            if (name.endsWith('.pdf')) return 'pdf';
+            if (name.endsWith('.xlsx') || name.endsWith('.xls')) return 'excel';
+            if (name.endsWith('.docx')) return 'word';
+            if (name.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) return 'image';
+            return 'unknown';
+        },
+
+        // 选择文件
+        async selectFile(file) {
+
+            console.log(file)
+            this.currentFile = file;
+            this.loading = true;
+            this.loadProgress = 0;
+
+            // 重置状态
+            this.pdfPageNum = 1;
+            this.pdfTotalPages = 0;
+            this.pdfDoc = null;
+            this.pdfError = '';
+            this.imageScale = 1;
+            this.wordContent = '';
+
+            // 重置Excel状态
+            this.workbook = null;
+            this.sheets = [];
+            this.pickedSheet = '';
+            this.tableData = [];
+            this.mergeMap = {};
+            this.originalMerges = [];
+
+            try {
+                switch (file.type) {
+                    case 'pdf':
+                        await this.loadPdf(file.url);
+                        break;
+                    case 'excel':
+                        await this.readExcel(file.file || file);
+                        break;
+                    case 'word':
+                        await this.loadWord(file.file || file.url);
+                        break;
+                    case 'image':
+                        // 图片直接通过URL显示,无需特殊处理
+                        break;
+                    default:
+                        console.warn('不支持的文件类型:', file.type);
+                }
+            } catch (error) {
+                console.error('文件加载失败:', error);
+                this.pdfError = this.getPdfErrorMessage(error);
+            } finally {
+                this.loading = false;
+            }
+        },
+
+        // 加载PDF - 修复版本
+        async loadPdf(url) {
+            try {
+                // 创建加载任务
+                const loadingTask = pdfjsLib.getDocument({
+                    url: url,
+                    // 添加进度回调
+                    progressCallback: (progress) => {
+                        this.loadProgress = Math.round((progress.loaded / progress.total) * 100);
+                    }
+                });
+
+                // 加载PDF文档
+                this.pdfDoc = await loadingTask.promise;
+                this.pdfTotalPages = this.pdfDoc.numPages;
+
+                // 使用$nextTick确保canvas已经渲染
+                this.$nextTick(() => {
+                    this.renderPdfPage(this.pdfPageNum);
+                });
+
+            } catch (error) {
+                console.error('PDF加载失败:', error);
+                this.pdfError = this.getPdfErrorMessage(error);
+                throw error;
+            }
+        },
+
+        // 渲染PDF页面 - 修复版本
+        async renderPdfPage(pageNum) {
+            try {
+                if (!this.pdfDoc) return;
+
+                const page = await this.pdfDoc.getPage(pageNum);
+                const canvas = this.$refs.pdfCanvas;
+                if (!canvas) {
+                    console.error('Canvas element not found');
+                    return;
+                }
+                const ctx = canvas.getContext('2d');
+
+                // 获取原始视图
+                const viewport = page.getViewport({ scale: 1 });
+
+                let scale;
+                if (this.scaleMode === 'width') {
+                    // 适应宽度
+                    const container = document.querySelector('.pdf-viewer');
+                    const containerWidth = container ? container.clientWidth - 40 : 800;
+                    scale = containerWidth / viewport.width;
+                } else {
+                    // 使用选择的缩放比例
+                    scale = parseFloat(this.pdfScale);
+                }
+
+                // 应用缩放
+                const scaledViewport = page.getViewport({ scale: scale });
+
+                // 设置Canvas尺寸
+                canvas.height = scaledViewport.height;
+                canvas.width = scaledViewport.width;
+
+                // 渲染配置
+                const renderContext = {
+                    canvasContext: ctx,
+                    viewport: scaledViewport
+                };
+
+                // 渲染页面
+                await page.render(renderContext).promise;
+
+            } catch (error) {
+                console.error('PDF渲染失败:', error);
+                this.pdfError = '页面渲染失败: ' + error.message;
+                throw error;
+            }
+        },
+
+        // PDF上一页
+        async prevPage() {
+            if (this.pdfPageNum > 1) {
+                this.pdfPageNum--;
+                await this.renderPdfPage(this.pdfPageNum);
+            }
+        },
+
+        // PDF下一页
+        async nextPage() {
+            if (this.pdfPageNum < this.pdfTotalPages) {
+                this.pdfPageNum++;
+                await this.renderPdfPage(this.pdfPageNum);
+            }
+        },
+
+        // 适应宽度
+        fitToWidth() {
+            this.scaleMode = 'width';
+            this.renderPdfPage(this.pdfPageNum);
+        },
+
+        // 处理缩放比例变化
+        handleScaleChange() {
+            this.scaleMode = 'scale';
+            this.renderPdfPage(this.pdfPageNum);
+        },
+
+        // 重试加载PDF
+        retryLoadPdf() {
+            if (this.currentFile.url) {
+                this.loadPdf(this.currentFile.url);
+            }
+        },
+
+        // 获取PDF错误信息
+        getPdfErrorMessage(error) {
+            if (error.name === 'PasswordException') {
+                return 'PDF文件受密码保护';
+            } else if (error.name === 'InvalidPDFException') {
+                return '无效的PDF文件';
+            } else if (error.name === 'MissingPDFException') {
+                return 'PDF文件不存在';
+            } else if (error.name === 'UnexpectedResponseException') {
+                return '服务器响应异常';
+            } else if (error.message.includes('NetworkError')) {
+                return '网络错误,请检查文件URL';
+            } else {
+                return '加载失败: ' + error.message;
+            }
+        },
+
+
+        // Excel相关方法
+        readExcel(file) {
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader()
+                reader.onload = e => {
+                    try {
+                        this.workbook = XLSX.read(e.target.result, { type: 'array' })
+                        this.sheets = this.workbook.SheetNames.map(name => ({ name }))
+                        this.pickedSheet = this.sheets[0].name
+                        this.changeSheet(this.pickedSheet)
+                        resolve(false)
+                    } catch (error) {
+                        reject(error)
+                    }
+                }
+                reader.onerror = err => reject(err)
+                reader.readAsArrayBuffer(file)
+            })
+        },
+
+        changeSheet(name) {
+
+            const ws = this.workbook.Sheets[name]
+            if (!ws) return
+            const { data, merges } = this.parseSheet(ws)
+            if (data.length == 0) {
+                console.log(123)
+                this.tableData = []
+            } else {
+                console.log(456)
+                this.tableData = data
+                this.originalMerges = merges
+                this.mergeMap = this.buildMergeMap(merges)
+                this.$nextTick(() => {
+                    this.initTableLayout()
+                })
+            }
+
+        },
+
+        initTableLayout() {
+            // 确保表格有足够的宽度来触发横向滚动
+            const table = this.$refs.excelTable
+            if (table) {
+                // 强制设置表格最小宽度,确保横向滚动生效
+                const minTableWidth = Math.max(table.scrollWidth, 1200) // 至少1200px或实际内容宽度
+                table.style.minWidth = minTableWidth + 'px'
+            }
+        },
+
+        updateScrollbar() {
+            const table = this.$refs.excelTable
+            const scrollbarTrack = this.$refs.scrollbarTrack
+
+            if (table && scrollbarTrack) {
+                // 设置滚动条轨道宽度与表格实际宽度一致
+                scrollbarTrack.style.width = table.scrollWidth + 'px'
+            }
+        },
+
+        parseSheet(ws) {
+            const range = XLSX.utils.decode_range(ws['!ref'])
+            const data = []
+
+            for (let r = range.s.r; r <= range.e.r; r++) {
+                const row = []
+                for (let c = range.s.c; c <= range.e.c; c++) {
+                    const cellAddress = XLSX.utils.encode_cell({ r, c })
+                    const cell = ws[cellAddress]
+                    row.push(cell ? cell.v : '')
+                }
+                data.push(row)
+            }
+
+            const merges = ws['!merges'] || []
+            return { data, merges }
+        },
+
+        buildMergeMap(merges) {
+            const map = {}
+            merges.forEach(merge => {
+                const { s, e } = merge
+
+                map[`${s.r}-${s.c}`] = {
+                    rowspan: e.r - s.r + 1,
+                    colspan: e.c - s.c + 1,
+                    isMaster: true
+                }
+
+                for (let r = s.r; r <= e.r; r++) {
+                    for (let c = s.c; c <= e.c; c++) {
+                        if (r !== s.r || c !== s.c) {
+                            map[`${r}-${c}`] = {
+                                hidden: true
+                            }
+                        }
+                    }
+                }
+            })
+            return map
+        },
+
+        getRowspan(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`]
+            return cellInfo && cellInfo.rowspan ? cellInfo.rowspan : 1
+        },
+
+        getColspan(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`]
+            return cellInfo && cellInfo.colspan ? cellInfo.colspan : 1
+        },
+
+        shouldShowCell(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`]
+            return !cellInfo || !cellInfo.hidden
+        },
+
+
+
+        // 加载Word
+        async loadWord(fileOrUrl) {
+            try {
+                let arrayBuffer;
+
+                if (fileOrUrl instanceof File) {
+                    arrayBuffer = await this.fileToArrayBuffer(fileOrUrl);
+                } else {
+                    const response = await fetch(fileOrUrl);
+                    arrayBuffer = await response.arrayBuffer();
+                }
+
+                // 这里需要引入mammoth库来处理Word文档
+                const result = await mammoth.convertToHtml({ arrayBuffer });
+                this.wordContent = result.value;
+
+                // 暂时模拟Word内容
+                // this.wordContent = '<p>Word文档预览功能需要引入mammoth.js库</p><p>当前文件: ' + this.currentFile.name + '</p>';
+
+                if (!this.wordContent) {
+                    throw new Error('Word文档转换失败');
+                }
+            } catch (error) {
+                console.error('Word加载失败:', error);
+                throw error;
+            }
+        },
+
+        // File对象转ArrayBuffer
+        fileToArrayBuffer(file) {
+            return new Promise((resolve, reject) => {
+                const reader = new FileReader();
+                reader.onload = () => resolve(reader.result);
+                reader.onerror = reject;
+                reader.readAsArrayBuffer(file);
+            });
+        },
+
+        // 图片缩放
+        zoomIn() {
+            if (this.imageScale < 3) {
+                this.imageScale += 0.1;
+            }
+        },
+
+        zoomOut() {
+            if (this.imageScale > 0.5) {
+                this.imageScale -= 0.1;
+            }
+        },
+
+        resetZoom() {
+            this.imageScale = 1;
+        },
+
+        // 下载文件
+        downloadFile() {
+            if (this.currentFile.file) {
+                // 下载上传的文件
+                const url = URL.createObjectURL(this.currentFile.file);
+                const a = document.createElement('a');
+                a.href = url;
+                a.download = this.currentFile.name;
+                document.body.appendChild(a);
+                a.click();
+                document.body.removeChild(a);
+                URL.revokeObjectURL(url);
+            } else if (this.currentFile.url) {
+                // 下载远程文件
+                const a = document.createElement('a');
+                a.href = this.currentFile.url;
+                a.download = this.currentFile.name;
+                document.body.appendChild(a);
+                a.click();
+                document.body.removeChild(a);
+            }
+        },
+
+        // 删除文件
+        removeFile(fileId) {
+            const index = this.files.findIndex(f => f.id === fileId);
+            if (index !== -1) {
+                // 如果删除的是当前选中的文件,清空当前文件
+                if (this.currentFile.id === fileId) {
+                    this.currentFile = {};
+                }
+                this.files.splice(index, 1);
+            }
+        }
+    }
+};
+</script>
+
+<style scoped>
+/* 其他原有样式保持不变 */
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+body {
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    min-height: 100vh;
+    padding: 20px;
+}
+
+.container {
+    max-width: 1800px;
+    margin: 0 auto;
+    background-color: white;
+    border-radius: 12px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+    overflow: hidden;
+}
+
+header {
+    background: linear-gradient(90deg, #4b6cb7 0%, #182848 100%);
+    color: white;
+    padding: 20px 30px;
+    text-align: center;
+}
+
+h1 {
+    font-size: 28px;
+    margin-bottom: 10px;
+}
+
+.subtitle {
+    font-size: 16px;
+    opacity: 0.8;
+}
+
+.main-content {
+    display: flex;
+    min-height: 600px;
+}
+
+.sidebar {
+    width: 250px;
+    background-color: #f8f9fa;
+    border-right: 1px solid #eaeaea;
+    padding: 20px;
+}
+
+.upload-section {
+    margin-bottom: 20px;
+}
+
+.upload-btn {
+    width: 100%;
+    padding: 12px;
+    background: #4b6cb7;
+    color: white;
+    border: none;
+    border-radius: 6px;
+    cursor: pointer;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    transition: background 0.3s;
+}
+
+.upload-btn:hover {
+    background: #3a5795;
+}
+
+.file-list {
+    list-style: none;
+}
+
+.file-item {
+    padding: 12px 15px;
+    margin-bottom: 8px;
+    background-color: white;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    border: 1px solid #eaeaea;
+    display: flex;
+    align-items: center;
+    position: relative;
+}
+
+.file-item:hover {
+    background-color: #e9ecef;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+}
+
+.file-item.active {
+    background-color: #4b6cb7;
+    color: white;
+    border-color: #4b6cb7;
+}
+
+.file-icon {
+    margin-right: 10px;
+    font-size: 18px;
+}
+
+.file-info {
+    flex: 1;
+}
+
+.file-name {
+    font-weight: 500;
+    margin-bottom: 4px;
+}
+
+.file-size {
+    font-size: 12px;
+    color: #6c757d;
+}
+
+.file-item.active .file-size {
+    color: rgba(255, 255, 255, 0.8);
+}
+
+.remove-btn {
+    background: none;
+    border: none;
+    font-size: 18px;
+    color: #dc3545;
+    cursor: pointer;
+    opacity: 0.7;
+    padding: 0 5px;
+}
+
+.remove-btn:hover {
+    opacity: 1;
+}
+
+.file-item.active .remove-btn {
+    color: white;
+}
+
+.preview-area {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+.preview-header {
+    padding: 15px 20px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.preview-header h3 {
+    font-size: 18px;
+    color: #333;
+}
+
+.file-actions {
+    display: flex;
+    gap: 10px;
+}
+
+.action-btn {
+    padding: 8px 15px;
+    background: #f8f9fa;
+    border: 1px solid #dee2e6;
+    border-radius: 4px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 5px;
+    transition: all 0.3s;
+}
+
+.action-btn:hover {
+    background: #e9ecef;
+}
+
+.preview-container {
+    flex: 1;
+    padding: 20px;
+    overflow: auto;
+    background-color: #f8f9fa;
+}
+
+.preview-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.preview-placeholder {
+    text-align: center;
+    color: #6c757d;
+}
+
+.placeholder-icon {
+    font-size: 64px;
+    margin-bottom: 15px;
+}
+
+.preview-placeholder h3 {
+    margin-bottom: 10px;
+    font-size: 20px;
+}
+
+.upload-here-btn {
+    margin-top: 15px;
+    padding: 10px 20px;
+    background: #4b6cb7;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background 0.3s;
+}
+
+.upload-here-btn:hover {
+    background: #3a5795;
+}
+
+.loading {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    height: 200px;
+    color: #6c757d;
+}
+
+.spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid rgba(0, 0, 0, 0.1);
+    border-left-color: #4b6cb7;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 15px;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+.pdf-preview {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.pdf-controls {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 15px;
+    gap: 10px;
+    flex-wrap: wrap;
+}
+
+.pdf-controls button {
+    padding: 8px 15px;
+    background-color: #4b6cb7;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.3s;
+    display: flex;
+    align-items: center;
+    gap: 5px;
+}
+
+.pdf-controls button:disabled {
+    background-color: #adb5bd;
+    cursor: not-allowed;
+}
+
+.pdf-controls button:hover:not(:disabled) {
+    background-color: #3a5795;
+}
+
+.page-info {
+    margin: 0 10px;
+    font-weight: 500;
+}
+
+.scale-select {
+    padding: 8px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+}
+
+.pdf-viewer {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    overflow: auto;
+    background: white;
+    border-radius: 8px;
+    padding: 20px;
+    min-height: 500px;
+    border: 1px solid #eaeaea;
+}
+
+.image-preview-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+.image-preview {
+    max-width: 100%;
+    max-height: 80%;
+    object-fit: contain;
+    transition: transform 0.3s;
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.image-actions {
+    margin-top: 15px;
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+
+.image-actions button {
+    padding: 8px 15px;
+    background-color: #f8f9fa;
+    border: 1px solid #dee2e6;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.image-actions button:hover {
+    background-color: #e9ecef;
+}
+
+.image-actions button:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+
+.zoom-level {
+    margin-left: 10px;
+    font-weight: 500;
+}
+
+
+
+.word-preview {
+    width: 100%;
+    height: 100%;
+}
+
+.word-content {
+    width: 100%;
+    background-color: white;
+    padding: 30px;
+    border-radius: 4px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+    line-height: 1.6;
+}
+
+.word-content h1,
+.word-content h2,
+.word-content h3 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+}
+
+.word-content p {
+    margin-bottom: 1em;
+}
+
+.word-content table {
+    border-collapse: collapse;
+    width: 100%;
+    margin: 1em 0;
+}
+
+.word-content table,
+.word-content th,
+.word-content td {
+    border: 1px solid #ddd;
+    padding: 8px;
+}
+
+.word-content th {
+    background-color: #f2f2f2;
+}
+
+.no-data,
+.no-content {
+    text-align: center;
+    padding: 40px;
+    color: #6c757d;
+}
+
+.unsupported-file {
+    text-align: center;
+    padding: 40px;
+}
+
+.unsupported-icon {
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.error-message {
+    text-align: center;
+    padding: 20px;
+    color: #dc3545;
+    background: #f8d7da;
+    border-radius: 8px;
+    margin: 20px 0;
+}
+
+
+/* Excel预览样式优化 */
+.excel-preview {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+/* 工作表标签样式 */
+.sheet-tabs {
+    margin-bottom: 15px;
+    border-bottom: 1px solid #eaeaea;
+}
+
+.tabs-container {
+    display: flex;
+    overflow-x: auto;
+    gap: 2px;
+}
+
+.tab-item {
+    padding: 8px 16px;
+    background: #f5f5f5;
+    border: 1px solid #ddd;
+    border-bottom: none;
+    border-radius: 4px 4px 0 0;
+    cursor: pointer;
+    white-space: nowrap;
+    transition: all 0.3s ease;
+}
+
+.tab-item:hover {
+    background: #e9e9e9;
+}
+
+.tab-item.active {
+    background: #4b6cb7;
+    color: white;
+    border-color: #4b6cb7;
+}
+
+.tab-name {
+    font-size: 14px;
+    font-weight: 500;
+}
+
+/* Excel信息栏 */
+.excel-info {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    padding: 12px;
+    box-sizing: border-box;
+    background: #f8f9fa;
+    border-radius: 6px;
+}
+
+.info-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.info-item .label {
+    font-weight: 600;
+    color: #495057;
+    font-size: 14px;
+}
+
+.info-item .value {
+    color: #4b6cb7;
+    font-weight: 500;
+    font-size: 14px;
+}
+
+/* 表格容器 */
+.table-container {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    border: 1px solid #e0e0e0;
+    border-radius: 6px;
+    background: white;
+    overflow: hidden;
+}
+
+/* 横向滚动条 */
+.horizontal-scrollbar {
+    height: 12px;
+    background: #f5f5f5;
+    border-bottom: 1px solid #e0e0e0;
+    overflow-x: auto;
+    overflow-y: hidden;
+}
+
+.scrollbar-track {
+    height: 1px;
+}
+
+/* 表格包装器 */
+.table-wrapper {
+    flex: 1;
+    overflow: auto;
+    position: relative;
+}
+
+/* 表格样式 */
+.excel-table {
+    border-collapse: collapse;
+    table-layout: fixed;
+    min-width: 100%;
+    font-size: 13px;
+}
+
+.excel-table tr {
+    height: 35px;
+}
+
+.excel-table td {
+    border: 1px solid #e0e0e0;
+    padding: 4px 8px;
+    vertical-align: middle;
+    position: relative;
+    min-width: 80px;
+    max-width: 300px;
+    word-wrap: break-word;
+    overflow: hidden;
+}
+
+.excel-table {
+    border-collapse: collapse;
+    table-layout: auto;
+    /* 改为自动布局,列宽由内容决定 */
+    min-width: 100%;
+    font-size: 13px;
+}
+
+.excel-table td {
+    min-width: 100px;
+    max-width: 300px;
+    border: 1px solid #e0e0e0;
+    padding: 4px 8px;
+    vertical-align: middle;
+    position: relative;
+    word-wrap: break-word;
+    overflow: hidden;
+}
+
+/* 表头行样式 */
+.header-row {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+}
+
+.header-cell {
+    background: #f8f9fa !important;
+    font-weight: 600;
+    color: #2c3e50;
+    border-bottom: 2px solid #4b6cb7 !important;
+}
+
+/* 数据单元格样式 */
+.data-cell {
+    background: white;
+}
+
+.data-cell:nth-child(even) {
+    background: #fafafa;
+}
+
+.data-cell:hover {
+    background: #f0f7ff !important;
+}
+
+/* 单元格内容 */
+.cell-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+/* 滚动条样式 */
+.table-wrapper::-webkit-scrollbar {
+    width: 12px;
+    height: 12px;
+}
+
+.table-wrapper::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 6px;
+}
+
+.table-wrapper::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 6px;
+    border: 2px solid #f1f1f1;
+}
+
+.table-wrapper::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+.table-wrapper::-webkit-scrollbar-corner {
+    background: #f1f1f1;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar {
+    height: 8px;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 4px;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+/* 空状态 */
+.empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: #909399;
+}
+
+.empty-icon {
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.empty-state h3 {
+    margin-bottom: 10px;
+    font-size: 18px;
+}
+
+footer {
+    text-align: center;
+    padding: 15px;
+    border-top: 1px solid #eaeaea;
+    color: #6c757d;
+    font-size: 14px;
+}
+</style>
+
+
+<style scoped>
+/* Excel预览样式优化 */
+.excel-preview {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+/* 工作表标签样式 */
+.sheet-tabs {
+    margin-bottom: 15px;
+    border-bottom: 1px solid #eaeaea;
+}
+
+.tabs-container {
+    display: flex;
+    overflow-x: auto;
+    gap: 2px;
+    padding-bottom: 2px;
+}
+
+.tab-item {
+    padding: 8px 16px;
+    background: #f5f5f5;
+    border: 1px solid #ddd;
+    border-bottom: none;
+    border-radius: 4px 4px 0 0;
+    cursor: pointer;
+    white-space: nowrap;
+    transition: all 0.3s ease;
+    flex-shrink: 0;
+}
+
+.tab-item:hover {
+    background: #e9e9e9;
+}
+
+.tab-item.active {
+    background: #4b6cb7;
+    color: white;
+    border-color: #4b6cb7;
+}
+
+.tab-name {
+    font-size: 14px;
+    font-weight: 500;
+}
+
+/* Excel信息栏 */
+.excel-info {
+    display: flex;
+    gap: 20px;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 6px;
+    margin-bottom: 15px;
+    flex-wrap: wrap;
+}
+
+.info-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.info-item .label {
+    font-weight: 600;
+    color: #495057;
+    font-size: 14px;
+}
+
+.info-item .value {
+    color: #4b6cb7;
+    font-weight: 500;
+    font-size: 14px;
+}
+
+/* 表格滚动容器 - 修复横向滚动问题 */
+.table-scroll-container {
+    flex: 1;
+    border: 1px solid #e0e0e0;
+    border-radius: 6px;
+    background: white;
+    overflow: auto;
+    position: relative;
+    min-height: 400px;
+}
+
+/* 表格包装器 */
+.table-wrapper {
+    min-width: 100%;
+    display: inline-block;
+}
+
+/* 表格样式 */
+.excel-table {
+    border-collapse: collapse;
+    table-layout: auto;
+    width: auto;
+    min-width: 100%;
+    font-size: 13px;
+    white-space: nowrap;
+}
+
+.excel-table tr {
+    height: 35px;
+}
+
+.excel-table td {
+    border: 1px solid #e0e0e0;
+    padding: 4px 8px;
+    vertical-align: middle;
+    position: relative;
+    min-width: 120px;
+    max-width: 300px;
+    word-wrap: break-word;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    background: white;
+}
+
+/* 表头行样式 */
+.header-row {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+}
+
+.header-cell {
+    background: #f8f9fa !important;
+    font-weight: 600;
+    color: #2c3e50;
+    border-bottom: 2px solid #4b6cb7 !important;
+    position: sticky;
+    top: 0;
+}
+
+/* 数据单元格样式 */
+.data-cell:nth-child(even) {
+    background: #fafafa;
+}
+
+.data-cell:hover {
+    background: #f0f7ff !important;
+}
+
+/* 单元格内容 */
+.cell-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+/* 滚动条样式 - 确保滚动条可见 */
+.table-scroll-container::-webkit-scrollbar {
+    width: 12px;
+    height: 12px;
+}
+
+.table-scroll-container::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 6px;
+}
+
+.table-scroll-container::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 6px;
+    border: 2px solid #f1f1f1;
+}
+
+.table-scroll-container::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+.table-scroll-container::-webkit-scrollbar-corner {
+    background: #f1f1f1;
+}
+
+/* 空状态 */
+.empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: #909399;
+}
+
+.empty-icon {
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.empty-state h3 {
+    margin-bottom: 10px;
+    font-size: 18px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .excel-info {
+        flex-direction: column;
+        gap: 8px;
+    }
+
+    .info-item {
+        justify-content: space-between;
+    }
+
+    .tab-item {
+        padding: 6px 12px;
+        font-size: 13px;
+    }
+
+    .excel-table {
+        font-size: 12px;
+    }
+
+    .excel-table td {
+        padding: 3px 6px;
+        min-width: 100px;
+    }
+
+    .table-scroll-container {
+        min-height: 300px;
+    }
+}
+
+/* 确保表格容器有足够的空间显示滚动条 */
+.preview-container {
+    flex: 1;
+    padding: 20px;
+    overflow: hidden;
+    /* 改为hidden防止外层滚动干扰 */
+    background-color: #f8f9fa;
+    display: flex;
+    flex-direction: column;
+}
+
+
+
+.preview-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+.excel-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+
+</style>

+ 13 - 13
src/okr/views/targetBusiness/companyTargetList.vue

@@ -59,7 +59,9 @@
 
                 <vxe-column title="周期" min-width="200" :visible="isEdit" align="center">
                     <template #default="{ row, rowIndex }">
-                        <span>{{ row.startDate + '-' + row.endDate }}</span>
+                        <span :class="[isEdit ? 'cursor-pointer' : '']"
+                            @click="isEdit && openChooseDateDialog(row, rowIndex)">{{ row.startDate + '-' + row.endDate
+                            }}</span>
                     </template>
                 </vxe-column>
 
@@ -197,7 +199,7 @@
                         <el-popover v-else ref="popoverRef" placement="bottom" trigger="click">
                             <template #reference>
                                 <span v-if="isEdit && row.type !== 1" class="cursor-pointer-center">{{ row.pname || '--'
-                                }}</span>
+                                    }}</span>
                             </template>
                             <div v-if="isEdit" class="dropdown-box">
                                 <el-link v-if="row.type !== 1"
@@ -289,7 +291,7 @@
 
         <!-- 上传附件 -->
         <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="currentRow.goalId"
-            :files="files" @confirm="confirmAppendFile" />
+            :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
 
         <SelectSeniorTargetDialog :visible.sync="chooseSeniorDialogVisible" :selected="currentRow"
             :type="currentTargetType" :scopeId="scopeId" @confirm="confirmChooseSeniorTarget" />
@@ -516,7 +518,6 @@ export default {
         },
 
         dateChoose(cycleType) {
-            console.log(cycleType)
             if (this.currentDateChoose == cycleType) return
             this.currentDateChoose = cycleType
             this.cycleType = cycleType
@@ -623,13 +624,15 @@ export default {
 
             if (list && list.length > 0) {
                 this.cycleList = list.filter(item => item.status == 1)
-                this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
-                if (this.filterCycleList && this.filterCycleList.length > 0) {
-                    this.filterCycleList.forEach(item => item.scopeId = item.scopeId.toString())
-                    if (!this.scopeId) this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
+                this.cycleList.forEach(item => item.scopeId = item.scopeId.toString())
+                if (this.scopeId) {
+                    let cycleType = this.cycleList.find(cycle => cycle.scopeId == this.scopeId).cycleType || 4
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == cycleType)
+                } else {
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
+                    this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
                 }
-            }
-            console.log("获取周期列表成功")
+            } 
         },
 
 
@@ -1416,9 +1419,6 @@ export default {
         }
 
     }
-
-
-
 }
 
 

+ 0 - 4
src/okr/views/targetBusiness/components/AddCycle.vue

@@ -212,7 +212,6 @@ export default {
                 this.form.endDate = moment(`${this.form.year}-12-31`).endOf('year').format('YYYY-MM-DD')
             }
             this.dateRange = [this.form.startDate, this.form.endDate]
-            console.log(this.dateRange);
         },
 
         quarterChange(value) {
@@ -230,7 +229,6 @@ export default {
                 this.form.endDate = moment(`${this.form.year}-12-31`).endOf('year').format('YYYY-MM-DD')
             }
             this.dateRange = [this.form.startDate, this.form.endDate]
-            console.log(this.dateRange);
         },
 
         monthChange(value) {
@@ -238,7 +236,6 @@ export default {
             this.form.startDate = moment(`${this.form.year}-${month}-01`).startOf('month').format('YYYY-MM-DD')
             this.form.endDate = moment(`${this.form.year}-${month}-01`).endOf('month').format('YYYY-MM-DD')
             this.dateRange = [this.form.startDate, this.form.endDate]
-            console.log(this.dateRange);
         },
 
         // 表单提交
@@ -275,7 +272,6 @@ export default {
                     let params = {
                         ...this.form, cycleValue: this.cycleValue
                     }
-                    console.log(params);
                     this.$http.post(url, params).then(res => {
 
                         if (res.code == 1) {

+ 8 - 7
src/okr/views/targetBusiness/components/AddTarget.vue

@@ -118,11 +118,13 @@
                             style="width: 300px;"></el-input>
                     </el-form-item>
 
-                    <el-form-item label="红线值" prop="riskValue">
-                        <el-input type="number" v-model="form.riskValue" placeholder="红线值"
+                    <el-form-item label="挑战值" prop="challengeValue">
+                        <el-input type="number" v-model="form.challengeValue" placeholder="挑战值"
                             style="width: 300px;"></el-input>
                     </el-form-item>
 
+
+
                 </div>
 
                 <div class="flex-box-ce">
@@ -131,16 +133,16 @@
                             style="width: 300px;"></el-input>
                     </el-form-item>
 
-                    <el-form-item label="挑战值" prop="challengeValue">
-                        <el-input type="number" v-model="form.challengeValue" placeholder="挑战值"
+                    <el-form-item label="预估达成值" prop="projectedValue">
+                        <el-input type="number" v-model="form.projectedValue" placeholder="预估达成值"
                             style="width: 300px;"></el-input>
                     </el-form-item>
                 </div>
 
 
                 <div class="flex-box-ce">
-                    <el-form-item label="预估达成值" prop="projectedValue">
-                        <el-input type="number" v-model="form.projectedValue" placeholder="预估达成值"
+                    <el-form-item label="红线值" prop="riskValue">
+                        <el-input type="number" v-model="form.riskValue" placeholder="红线值"
                             style="width: 300px;"></el-input>
                     </el-form-item>
                 </div>
@@ -393,7 +395,6 @@ export default {
                 let data = res.data.data;
                 data.reverse()
                 this.unitList = data;
-                console.log(this.unitList);
             })
         },
         // 获取公司部门列表

+ 394 - 0
src/okr/views/targetBusiness/components/AppendFileUpload copy 2.vue

@@ -0,0 +1,394 @@
+<template>
+
+    <el-dialog title="附件上传" center :visible.sync="fileUploadVisible" width="600px" :before-close="dialogBeforeClose"
+        append-to-body :close-on-press-escape="false" :close-on-click-modal="false">
+        <div v-if="!isPreview" class="dialog-box scroll-bar" v-loading="loading">
+            <el-upload ref="upload" action="#" :multiple="true" :limit="5" :http-request="oss_upload" :headers="$xtoken"
+                :accept="acceptFile" :auto-upload="false" :on-preview="onFilePreView" :on-success="handleSuccess"
+                :before-upload="beforeFilesUpload" :file-list="fileList">
+                <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
+                <el-button style="margin-left: 10px;" size="small" type="success"
+                    @click="submitUpload">上传到服务器</el-button>
+            </el-upload>
+            <el-progress v-show="showProcess" :percentage="processLength" :stroke-width="2"></el-progress>
+        </div>
+
+        <div v-else class="preview-file-list">
+            <div class="file-item" @click="onFilePreView(file)" v-for="(file, index) in fileList" :key="index">
+                <i class="el-icon-document"></i>
+                {{ file.name }}
+            </div>
+        </div>
+
+        <div @click="onFilePreView(fileList[0])">{{ fileList }}</div>
+
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
+        <div slot="footer">
+            <el-button type="primary" size="small" @click="comfirm()">确 定</el-button>
+        </div>
+    </el-dialog>
+
+</template>
+
+<script>
+import axiosUpload from '@/utils/axiosUpload'
+import uploadOss from '@/components/upload';
+import { mapGetters } from 'vuex';
+import { _debounce } from '@/utils/auth';
+import axios from 'axios'   // axios 兼容 IE;如不需要可换回 fetch
+import previewFile from './previewFile.vue';
+import moment from 'moment'
+
+export default {
+    components: {
+        uploadOss,
+        previewFile
+    },
+    model: {
+        prop: 'fileUploadVisible',
+        event: 'close-dialog'
+    },
+    props: {
+        goalId: {
+            type: Number | String,
+            default: 0
+        },
+        fileUploadVisible: {
+            type: Boolean,
+            default: false
+        },
+        files: {
+            type: Array,
+            default: () => []
+        },
+        isAllowUpload: {
+            type: Boolean,
+            default: true
+        },
+        isPreview: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data() {
+        return {
+            loading: false,
+            fileList: [],
+            uploadFileList: [],
+            imgUrl: '',
+            uploadData: {
+                ...this.$xtoken
+            },
+            processLength: 0,
+            showProcess: false,
+            currentRow: null,
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF, .doc, .docx, .xls, .XLSX',
+            currentFile: null,
+            fileViewerDialogVisible: false,
+            config: null
+        }
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+    },
+
+    mounted() {
+        if (this.fileUploadVisible) {
+            this.uploadFileList = this.files
+            this.batchLoad();
+        } 
+    },
+
+    methods: {
+        onFilePreView(file) {
+            console.log(file)
+            this.currentFile = null;
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            let lastIndex = file.url && file.url.lastIndexOf("/") || -1
+            let suffix; //文件后缀名
+
+            if (lastIndex > 0) {
+                suffix = file.url.substr(lastIndex + 1, file.url.length - 1).split(".")[1];
+                if (imgFiles.includes(suffix)) {
+                    this.imgUrl = ''
+                    this.imgUrl = file.url;
+                    this.$viewerApi({
+                        images: [this.imgUrl]
+                    })
+                } else {
+                    this.currentFile = file
+                    this.fileViewerDialogVisible = true
+                    // window.open(file.url, '_blank');
+                }
+            }
+        },
+
+        
+
+        handleSuccess: _debounce(function (response, file, fileList) {
+            console.log("上传成功")
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        }),
+
+        handleRemove(file, fileList) {
+            this.fileList = fileList; // 用来显示的文件列表
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+
+        // 手动上传
+        submitUpload() {
+            this.$refs.upload.submit();
+        },
+        
+
+        get_sign(callback) {
+            // 测试添加 'https://intesys.cms.g107.com'
+            axiosUpload('get', 'https://intesys.cms.g107.com/integral.php/Api/get_signature').then(res => {
+                this.config = res.data.data
+                callback()
+            })
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+        oss_upload(upload_obj) {
+            this.get_sign(() => {
+                // this.beforeUpload_all(upload_obj.file).then(res=>{
+                //   this.upload(res)
+                // });
+                this.upload(upload_obj.file)
+            })
+        },
+
+        async upload(item) {
+            let self = this
+            const photo = item // 获取图片对象
+            const photoName = item.name // 原图片的名称
+            const url = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com'
+            let date = moment().format('YYYY/MM/DD')
+            let param = new FormData()
+            let site_id
+            if (this.coursePath) {
+                site_id = this.coursePath
+            } else {
+                site_id = this.$getCache('site_info').id
+            }
+            let randomStr = this.random_string(32)
+            let key = 'intesys/' + site_id + '/' + date + '/' + randomStr + '/' + photoName
+            // let loadingInstance = Loading.service({});
+            param.append('Filename', photoName)
+            param.append('key', key)
+            param.append('policy', this.config.policy)
+            param.append('OSSAccessKeyId', this.config.accessid)
+            param.append('success_action_status', '200') // 不要问为什么,照做
+            param.append('callback', this.config.callback)
+            param.append('signature', this.config.signature)
+            param.append('file', photo) // 这个**切记**一定要放到最后去 append ,不然阿里云会一直报 key 的错误
+            let response = await axios.post(url, param, {
+                headers: {
+                    'Content-Type': 'multipart/form-data'
+                }
+            })
+            if (response.data.Status == 'Ok') {
+                this.processLength = 100
+                setTimeout(() => {
+                    this.processLength = 0
+                    // this.$nextTick(() => { // 以服务的方式调用的 Loading 需要异步关闭
+                    //   loadingInstance.close();
+                    // });
+                }, 200)
+                self.fileList.push({
+                    name: randomStr + photoName, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, name: item.name, response: {
+                        url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key
+                    }
+                })
+                self.handleSuccess({ status: 1, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, file_name: randomStr + photoName }, item, self.fileList)
+            }
+            this.showProcess = false
+
+        },
+
+        random_string(len) {
+            len = len || 32
+            var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
+            var maxPos = chars.length
+            var pwd = ''
+            for (let i = 0; i < len; i++) {
+                pwd += chars.charAt(Math.floor(Math.random() * maxPos))
+            }
+            return pwd
+        },
+        
+
+        comfirmUploadFiles(files) {
+            let url = `/okr/og/goals/files/${this.user_info.site_id}`
+            let data = {
+                goalId: this.goalId,
+                files
+            }
+            this.$http.post(url, data).then(res => {
+                let { code } = res
+
+                if (code !== 1)
+                    return this.$message.error(res.message || "上传附件失败")
+                else {
+                    this.$emit('close-dialog', false)
+                    this.$emit('confirm', this.uploadFileList)
+                }
+                this.currentRow = res.data
+                
+            })
+        },
+
+       
+
+        async comfirm() {
+            // await this.submitUpload()
+
+            if (this.isAllowUpload) {
+                this.comfirmUploadFiles(this.uploadFileList)
+            } else {
+                this.$emit('close-dialog', false)
+                this.$emit('confirm', this.uploadFileList)
+            }
+            
+        },
+
+        dialogBeforeClose() {
+            this.$emit('close-dialog', false)
+        },
+
+
+        /* 主函数:接收 url 数组 → 批量回显 */
+        async batchLoad() {
+            const urlArr = this.files || []; // 已成功上传的文件列表
+            if (!urlArr.length) return
+
+            /* 并发下载并转成 File */
+            const taskList = urlArr.map(u => this.url2File(u))
+            try {
+                const files = await Promise.all(taskList)
+                this.fileList = files                // 一次性塞给 el-upload
+                // this.$message.success(`已回显 ${files.length} 个文件`)
+            } catch (e) {
+                this.$message.error('下载失败:' + e.message)
+            }
+        },
+
+        /* 单条 url → File → el-upload 对象 */
+        async url2File(url) {
+            this.loading = true
+            // 1. 下载成 blob(axios 写法,IE 可用)
+            const { data: blob, headers } = await axios.get(url, {
+                responseType: 'blob'
+            })
+
+            // 2. 从 Content-Disposition 或 url 取文件名
+            let fileName = 'unknown'
+            const disposition = headers['content-disposition']
+            if (disposition && disposition.includes('filename=')) {
+                fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''))
+            } else {
+                const temp = url.split('/').pop().split('?')[0]
+                fileName = decodeURIComponent(temp)
+            }
+
+            // 3. 生成 File 对象
+            const file = new File([blob], fileName, { type: blob.type })
+            this.loading = false
+            // 4. 返回 el-upload 需要的格式
+            return {
+                name: fileName,
+                url: url,        // 预览图
+                raw: file,       // File 对象
+                status: 'success'
+            }
+        },
+
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+    }
+
+}
+</script>
+
+
+<style scoped lang="scss">
+
+
+.dialog-box {
+    width: 100%;
+    max-height: 600px;
+    overflow-y: auto;
+}
+
+.preview-file-list {
+    .file-item {
+        margin-bottom: 10px;
+    }
+}
+</style>

+ 514 - 0
src/okr/views/targetBusiness/components/AppendFileUpload copy 3.vue

@@ -0,0 +1,514 @@
+<template>
+
+    <el-dialog :title="isPreview ? '预览附件' : '上传附件'" center :visible.sync="fileUploadVisible" width="600px"
+        :before-close="dialogBeforeClose" append-to-body :close-on-press-escape="false" :close-on-click-modal="false">
+        <div class="dialog-box scroll-bar" v-loading="loading">
+
+            <el-upload v-if="!isPreview" ref="upload" action="#" :multiple="true" :limit="5" :http-request="oss_upload"
+                :headers="$xtoken" :accept="acceptFile" :auto-upload="false" :on-change="handleChange"
+                :on-preview="onFilePreView" :on-remove="handleRemove" :on-success="handleSuccess"
+                :before-upload="beforeFilesUpload" :file-list="fileList">
+                <span v-if="fileList.length >= 5" class="fontColorC">
+                    <strong style="color: red;">*</strong>
+                    (附件上传数量已经达到上限)
+                </span>
+
+                <el-button v-if="fileList.length < 5 && isOwner" slot="trigger" size="small"
+                    type="primary">选取文件</el-button>
+
+            </el-upload>
+
+            <template v-else>
+                <div v-if="members && members.length > 0">
+                    <div class="member-item" v-for="member in members" :key="member.employeeId">
+                        <div class="employee-name flex-box-ce" style="justify-content: space-between;">
+                            <span>
+                                <i class="el-icon-user"></i>
+                                {{ employeeMap[member.employeeId].name }}
+                            </span>
+                            &nbsp;
+                            &nbsp;
+                            <span>
+                                <i class="el-icon-time"></i>
+                                {{ member.updateTime }}
+                            </span>
+
+                        </div>
+                        <div class="file-item" v-for="file in member.files" :key="file">
+                            <el-link type="primary" @click="onFilePreView2(file)">
+                                <i class="el-icon-document"></i>
+                                {{ parseUrlFile(file).fileName + parseUrlFile(file).ext }}
+                            </el-link>
+                        </div>
+
+                    </div>
+                </div>
+
+                <div v-else class="fontColorC">暂无附件,去上传</div>
+
+            </template>
+
+        </div>
+
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
+        <div slot="footer">
+            <el-button type="primary" size="small" @click="comfirm()">确 定</el-button>
+            <!-- <el-button size="small" @click="cancel()">取 消</el-button> -->
+        </div>
+    </el-dialog>
+
+</template>
+
+
+
+<script>
+import axiosUpload from '@/utils/axiosUpload'
+import uploadOss from '@/components/upload';
+import { mapGetters } from 'vuex';
+import { _debounce } from '@/utils/auth';
+import axios from 'axios'   // axios 兼容 IE;如不需要可换回 fetch
+import previewFile from './previewFile.vue';
+import moment from 'moment'
+
+export default {
+    components: {
+        uploadOss,
+        previewFile
+    },
+    model: {
+        prop: 'fileUploadVisible',
+        event: 'close-dialog'
+    },
+    props: {
+        goalId: {
+            type: Number | String,
+            default: 0
+        },
+        fileUploadVisible: {
+            type: Boolean,
+            default: false
+        },
+        files: {
+            type: Array,
+            default: () => []
+        },
+        members: {
+            type: Array,
+            default: () => []
+        },
+        isAllowUpload: {
+            type: Boolean,
+            default: true
+        },
+        isPreview: {
+            type: Boolean,
+            default: false
+        },
+        ownerId: {
+            type: String | Number,
+            default: ''
+        },
+    },
+    data() {
+        return {
+            employeeMap: this.$getEmployeeMap(),
+            loading: false,
+            fileList: [],
+            uploadFileList: [],
+            imgUrl: '',
+            uploadData: {
+                ...this.$xtoken
+            },
+            initFiles: [],
+            processLength: 0,
+            showProcess: false,
+            currentRow: null,
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF, .doc, .docx, .xls, .XLSX',
+            currentFile: null,
+            fileViewerDialogVisible: false,
+            config: null,
+            lastAction: "",
+        }
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+        isOwner() {
+            return this.ownerId == this.user_info.id
+        }
+    },
+
+    mounted() {
+        if (this.fileUploadVisible) {
+            this.initFiles = this.files
+            this.uploadFileList = this.files
+            if (this.files.length > 0) {
+                this.fileList = this.files.map(file => ({
+                    uid: file,
+                    url: file,
+                    name: this.parseUrlFile(file).fileName + this.parseUrlFile(file).ext,
+                    type: this.parseUrlFile(file).ext.replace(".", ""),
+                }));
+                console.log(this.fileList)
+            }
+            // this.batchLoad();
+        }
+    },
+
+    methods: {
+
+        parseUrlFile(url) {
+            const [, fileName, ext] = url.match(/\/([^/?#]+)(\.\w+)(?:[?#]|$)/) || [];
+            return { fileName, ext };
+        },
+
+
+        onFilePreView(file) {
+            this.currentFile = null;
+            let index = file.name.indexOf("."); 
+            let suffix = file.name.substr(index + 1, file.url.length - 1); //文件后缀名
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            if (imgFiles.includes(suffix)) {
+                this.imgUrl = ''
+                this.imgUrl = file.url;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                let currentFile = {
+                    name: file.name,
+                    url: file.url,
+                    type: suffix
+                }
+                this.currentFile = currentFile
+                this.fileViewerDialogVisible = true
+                // window.open(file.url, '_blank');
+            }
+        },
+
+        onFilePreView2(fileUrl) {
+            this.currentFile = null;
+            let fileObj = this.parseUrlFile(fileUrl);
+            let type = fileObj.ext.replace(".", "")
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            if (imgFiles.includes(type)) {
+                this.imgUrl = ''
+                this.imgUrl = fileUrl;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                let currentFile = {
+                    name: fileObj.fileName + fileObj.ext,
+                    url: fileUrl,
+                    type
+                }
+                this.currentFile = currentFile;
+                this.fileViewerDialogVisible = true;
+            }
+        },
+
+
+
+        handleSuccess: _debounce(function (response, file, fileList) {
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+
+            if (this.isAllowUpload) {
+                this.comfirmUploadFiles(this.uploadFileList)
+            } else {
+                this.$emit('close-dialog', false)
+                this.$emit('confirm', this.uploadFileList)
+            }
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        }),
+
+        /* 新增文件(选择后) */
+        handleChange(file, fileList) {
+            console.log("文件改变了")
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                return this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                return this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            
+            this.lastAction = 'hasChange'
+            const url = URL.createObjectURL(file.raw)
+            file.url = url  
+            // this.fileList = fileList
+            // this.uploadFileList = this.fileList.map(item => {
+            //     return item.url;
+            // });
+        },
+
+        handleRemove(file, fileList) {
+            this.lastAction = 'hasChange'
+            URL.revokeObjectURL(file.url) // 释放内存
+            this.fileList = fileList; // 用来显示的文件列表
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+
+        // 手动上传
+        submitUpload() {
+            this.$refs.upload.submit();
+        },
+
+
+        get_sign(callback) {
+            // 测试添加 'https://intesys.cms.g107.com'
+            axiosUpload('get', 'https://intesys.cms.g107.com/integral.php/Api/get_signature').then(res => {
+                this.config = res.data.data
+                callback()
+            })
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+        oss_upload(upload_obj) {
+            this.get_sign(() => {
+                // this.beforeUpload_all(upload_obj.file).then(res=>{
+                //   this.upload(res)
+                // });
+                this.upload(upload_obj.file)
+            })
+        },
+
+        upload(item) {
+            let self = this
+            const photo = item // 获取图片对象
+            const photoName = item.name // 原图片的名称
+            const url = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com'
+            let date = moment().format('YYYY/MM/DD')
+            let param = new FormData()
+            let site_id
+            if (this.coursePath) {
+                site_id = this.coursePath
+            } else {
+                site_id = this.$getCache('site_info').id
+            }
+            let randomStr = this.random_string(32)
+            let key = 'intesys/' + site_id + '/' + date + '/' + randomStr + '/' + photoName
+            // let loadingInstance = Loading.service({});
+            param.append('Filename', photoName)
+            param.append('key', key)
+            param.append('policy', this.config.policy)
+            param.append('OSSAccessKeyId', this.config.accessid)
+            param.append('success_action_status', '200') // 不要问为什么,照做
+            param.append('callback', this.config.callback)
+            param.append('signature', this.config.signature)
+            param.append('file', photo) // 这个**切记**一定要放到最后去 append ,不然阿里云会一直报 key 的错误
+            axios.post(url, param, {
+                headers: {
+                    'Content-Type': 'multipart/form-data'
+                }
+            }).then(response => {
+                if (response.data.Status == 'Ok') {
+                    self.fileList.push({
+                        name: randomStr + photoName, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, name: item.name, response: {
+                            url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key
+                        }
+                    })
+                    self.handleSuccess({ status: 1, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, file_name: randomStr + photoName }, item, self.fileList)
+                }
+            })
+            
+            this.showProcess = false
+
+        },
+
+        random_string(len) {
+            len = len || 32
+            var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
+            var maxPos = chars.length
+            var pwd = ''
+            for (let i = 0; i < len; i++) {
+                pwd += chars.charAt(Math.floor(Math.random() * maxPos))
+            }
+            return pwd
+        },
+
+
+        comfirmUploadFiles(files) {
+            let url = `/okr/og/goals/files/${this.user_info.site_id}`
+            let data = {
+                goalId: this.goalId,
+                files
+            }
+            this.$http.post(url, data).then(res => {
+                let { code } = res
+
+                if (code !== 1)
+                    return this.$message.error(res.message || "上传附件失败")
+                else {
+                    this.$emit('close-dialog', false)
+                    this.$emit('confirm', this.uploadFileList)
+                }
+                this.currentRow = res.data
+
+            })
+        },
+
+        cancel() {
+            this.$emit('close-dialog', false)
+        },
+
+        comfirm() {
+            console.log(this.fileList)
+            // if (this.lastAction === 'hasChange') {
+            //     this.submitUpload()
+            //     // if (this.isAllowUpload) {
+            //     //     this.comfirmUploadFiles(this.uploadFileList)
+            //     // } else {
+            //     //     this.$emit('close-dialog', false)
+            //     //     this.$emit('confirm', this.uploadFileList)
+            //     // }
+            // }
+            // else
+            //     this.$emit('close-dialog', false)
+        },
+
+        dialogBeforeClose() {
+            this.$emit('close-dialog', false)
+        },
+
+
+        /* 主函数:接收 url 数组 → 批量回显 */
+        async batchLoad() {
+            const urlArr = this.files || []; // 已成功上传的文件列表
+            if (!urlArr.length) return
+
+            /* 并发下载并转成 File */
+            const taskList = urlArr.map(u => this.url2File(u))
+            try {
+                const files = await Promise.all(taskList)
+                this.fileList = files                // 一次性塞给 el-upload
+                // this.$message.success(`已回显 ${files.length} 个文件`)
+            } catch (e) {
+                this.$message.error('下载失败:' + e.message)
+            }
+        },
+
+        /* 单条 url → File → el-upload 对象 */
+        async url2File(url) {
+            // this.loading = true
+            // 1. 下载成 blob(axios 写法,IE 可用)
+            const { data: blob, headers } = await axios.get(url, {
+                responseType: 'blob'
+            })
+
+            // 2. 从 Content-Disposition 或 url 取文件名
+            let fileName = 'unknown'
+            const disposition = headers['content-disposition']
+            if (disposition && disposition.includes('filename=')) {
+                fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''))
+            } else {
+                const temp = url.split('/').pop().split('?')[0]
+                fileName = decodeURIComponent(temp)
+            }
+
+            // 3. 生成 File 对象
+            const file = new File([blob], fileName, { type: blob.type })
+            // this.loading = false
+            // 4. 返回 el-upload 需要的格式
+            return {
+                name: fileName,
+                url: url,        // 预览图
+                raw: file,       // File 对象
+                status: 'success'
+            }
+        },
+
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+    }
+
+}
+</script>
+
+
+<style scoped lang="scss">
+.dialog-box {
+    width: 100%;
+    max-height: 600px;
+    overflow-y: auto;
+}
+
+.member-item {
+    padding: 10px;
+    box-sizing: border-box;
+    border-bottom: 1px solid #f1f1f1;
+    .employee-name {
+        font-size: 14px;
+        margin-bottom: 10px;
+    }
+    .file-item {
+        margin-bottom: 10px;
+    }
+}
+</style>

+ 275 - 0
src/okr/views/targetBusiness/components/AppendFileUpload copy.vue

@@ -0,0 +1,275 @@
+<template>
+
+    <el-dialog title="附件上传" center :visible.sync="fileUploadVisible" width="600px" :before-close="dialogBeforeClose"
+        append-to-body :close-on-press-escape="false" :close-on-click-modal="false">
+        <div class="dialog-box scroll-bar" v-loading="loading">
+            <uploadOss :key="Date.now()" class="avatar-uploader" :headers="$xtoken" :show-file-list="true"
+                :multiple="true" :limit="5" :accept="acceptFile" :file-list="fileList" :action="$action"
+                :on-preview="onFilePreView" :on-success="handleSuccess" :on-remove="handleRemove"
+                :before-upload="beforeFilesUpload" :disabled="isPreview">
+                <el-button v-if="!isPreview" class="primaryBtn" icon="el-icon-paperclip" plain size="mini">上传附件</el-button>
+                
+                <!-- <div slot="tip" class="el-upload__tip">(支持上传xlsx,xls,doc,docx,pdf,txt,png,jpeg,jpg,gif, 大小不能超过5M)</div> -->
+            </uploadOss>
+        </div>
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
+        <div slot="footer">
+            <el-button type="primary" size="small" @click="comfirm()">确 定</el-button>
+        </div>
+    </el-dialog>
+
+</template>
+
+<script>
+import uploadOss from '@/components/upload';
+import { mapGetters } from 'vuex';
+import { _debounce } from '@/utils/auth';
+import axios from 'axios'   // axios 兼容 IE;如不需要可换回 fetch
+import previewFile from './previewFile.vue';
+export default {
+    components: {
+        uploadOss,
+        previewFile
+    },
+    model: {
+        prop: 'fileUploadVisible',
+        event: 'close-dialog'
+    },
+    props: {
+        goalId: {
+            type: Number | String,
+            default: 0
+        },
+        fileUploadVisible: {
+            type: Boolean,
+            default: false
+        },
+        files: {
+            type: Array,
+            default: () => []
+        },
+        isAllowUpload: {
+            type: Boolean,
+            default: true
+        },
+        isPreview: {
+            type: Boolean,
+            default: false
+        }
+    },
+
+    data() {
+        return {
+            loading: false,
+            fileList: [],
+            uploadFileList: [],
+            imgUrl: '',
+            currentRow: null,
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF, .doc, .docx, .xls, .XLSX',
+            currentFile: null,
+            fileViewerDialogVisible: false
+        }
+    },
+
+    computed: {
+        ...mapGetters(['user_info']),
+    },
+
+    mounted() {
+        if (this.fileUploadVisible) {
+            this.uploadFileList = this.files
+            this.batchLoad();
+        } 
+    },
+
+    methods: {
+        onFilePreView(file) {
+            console.log(file)
+            this.currentFile = null;
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            let lastIndex = file.url && file.url.lastIndexOf("/") || -1
+            let suffix; //文件后缀名
+
+            if (lastIndex > 0) {
+                suffix = file.url.substr(lastIndex + 1, file.url.length - 1).split(".")[1];
+                if (imgFiles.includes(suffix)) {
+                    this.imgUrl = ''
+                    this.imgUrl = file.url;
+                    this.$viewerApi({
+                        images: [this.imgUrl]
+                    })
+                } else {
+                    this.currentFile = file
+                    this.fileViewerDialogVisible = true
+                    // window.open(file.url, '_blank');
+                }
+            }
+        },
+
+        handleSuccess: _debounce(function (response, file, fileList) {
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        }),
+
+        handleRemove(file, fileList) {
+            this.fileList = fileList; // 用来显示的文件列表
+            this.uploadFileList = this.fileList.map(item => {
+                return item.url;
+            });
+            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+        
+        
+
+        comfirmUploadFiles(files) {
+            let url = `/okr/og/goals/files/${this.user_info.site_id}`
+            let data = {
+                goalId: this.goalId,
+                files
+            }
+            this.$http.post(url, data).then(res => {
+                let { code } = res
+
+                if (code !== 1)
+                    return this.$message.error(res.message || "上传附件失败")
+                else {
+                    this.$emit('close-dialog', false)
+                    this.$emit('confirm', this.uploadFileList)
+                }
+                this.currentRow = res.data
+                
+            })
+        },
+
+        comfirm() {
+            if (this.isAllowUpload) {
+                this.comfirmUploadFiles(this.uploadFileList)
+            } else {
+                this.$emit('close-dialog', false)
+                this.$emit('confirm', this.uploadFileList)
+            }
+            
+        },
+
+        dialogBeforeClose() {
+            this.$emit('close-dialog', false)
+        },
+
+
+        /* 主函数:接收 url 数组 → 批量回显 */
+        async batchLoad() {
+            const urlArr = this.files || []; // 已成功上传的文件列表
+            if (!urlArr.length) return
+
+            /* 并发下载并转成 File */
+            const taskList = urlArr.map(u => this.url2File(u))
+            try {
+                const files = await Promise.all(taskList)
+                this.fileList = files                // 一次性塞给 el-upload
+                // this.$message.success(`已回显 ${files.length} 个文件`)
+            } catch (e) {
+                this.$message.error('下载失败:' + e.message)
+            }
+        },
+
+        /* 单条 url → File → el-upload 对象 */
+        async url2File(url) {
+            this.loading = true
+            // 1. 下载成 blob(axios 写法,IE 可用)
+            const { data: blob, headers } = await axios.get(url, {
+                responseType: 'blob'
+            })
+
+            // 2. 从 Content-Disposition 或 url 取文件名
+            let fileName = 'unknown'
+            const disposition = headers['content-disposition']
+            if (disposition && disposition.includes('filename=')) {
+                fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''))
+            } else {
+                const temp = url.split('/').pop().split('?')[0]
+                fileName = decodeURIComponent(temp)
+            }
+
+            // 3. 生成 File 对象
+            const file = new File([blob], fileName, { type: blob.type })
+            this.loading = false
+            // 4. 返回 el-upload 需要的格式
+            return {
+                name: fileName,
+                url: url,        // 预览图
+                raw: file,       // File 对象
+                status: 'success'
+            }
+        },
+
+
+
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+    }
+
+}
+</script>
+
+<style lang="scss">
+.el-upload-list .el-upload-list__item .is-success .el-icon-close {
+    display: none !important;
+}
+
+</style>
+
+<style scoped lang="scss">
+
+
+.dialog-box {
+    width: 100%;
+    max-height: 600px;
+    overflow-y: auto;
+}
+</style>

+ 432 - 53
src/okr/views/targetBusiness/components/AppendFileUpload.vue

@@ -1,32 +1,88 @@
 <template>
+    
+    <el-dialog :title="isPreview ? '预览附件' : '上传附件'" center :visible.sync="fileUploadVisible" width="600px"
+        :before-close="dialogBeforeClose" append-to-body :close-on-press-escape="!loading" :close-on-click-modal="!loading" :show-close="!loading">
+        <div class="dialog-box scroll-bar" v-loading="loading">
+
+            <el-upload v-if="!isPreview" ref="upload" action="#" :multiple="true" :limit="5" :http-request="oss_upload"
+                :headers="$xtoken" :accept="acceptFile" :auto-upload="false" :on-change="handleChange"
+                :on-preview="onFilePreView" :on-remove="handleRemove" :on-success="handleSuccess"
+                :before-upload="beforeFilesUpload" :file-list="fileList">
+                <span v-if="fileList.length >= 5" class="fontColorC">
+                    <strong style="color: red;">*</strong>
+                    (附件上传数量已经达到上限)
+                </span>
+
+                <el-button v-if="fileList.length < 5 && isOwner" slot="trigger" size="small"
+                    type="primary">选取文件</el-button>
+
+            </el-upload>
+            
+
+            <template v-else>
+                <div v-if="members && members.length > 0">
+                    <div class="member-item" v-for="member in members" :key="member.employeeId">
+                        <div class="employee-name flex-box-ce" style="justify-content: space-between;">
+                            <span>
+                                <i class="el-icon-user"></i>
+                                {{ employeeMap[member.employeeId].name }}
+                            </span>
+                            &nbsp;
+                            &nbsp;
+                            <span>
+                                <i class="el-icon-time"></i>
+                                {{ member.updateTime }}
+                            </span>
+
+                        </div>
+                        <div class="file-item" v-for="file in member.files" :key="file">
+                            <el-link type="primary" @click="onFilePreView2(file)">
+                                <i class="el-icon-document"></i>
+                                {{ parseUrlFile(file).fileName + parseUrlFile(file).ext }}
+                            </el-link>
+                        </div>
+
+                    </div>
+                </div>
+
+                <div v-else class="fontColorC">暂无附件,去上传</div>
+
+            </template>
 
-    <el-dialog title="附件上传" center :visible.sync="fileUploadVisible" width="500px" :before-close="dialogBeforeClose"
-        append-to-body :close-on-press-escape="false" :close-on-click-modal="false">
-        <div v-loading="loading">
-        <uploadOss :key="Date.now()" class="avatar-uploader" :headers="$xtoken" :show-file-list="true" :multiple="true"
-            :limit="5" :accept="acceptFile" :file-list="fileList" :action="$action" :on-preview="onFilePreView"
-            :on-success="handleSuccess" :on-remove="handleRemove" :before-upload="beforeFilesUpload">
-            <el-button class="primaryBtn" icon="el-icon-paperclip" plain size="mini">上传附件</el-button>
-            <!-- <div slot="tip" class="el-upload__tip">(支持上传xlsx,xls,doc,docx,pdf,txt,png,jpeg,jpg,gif, 大小不能超过5M)</div> -->
-        </uploadOss>
         </div>
 
+        
+
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog>
+
         <div slot="footer">
-            <el-button type="primary" size="small" @click="comfirm()">确 定</el-button>
+            <el-button type="primary" :loading="btnLoading" size="small" @click="comfirm()">确 定</el-button>
+            <!-- <el-button size="small" @click="cancel()">取 消</el-button> -->
         </div>
     </el-dialog>
 
 </template>
 
+
+
 <script>
-import { _debounce } from '@/utils/auth';
+import axiosUpload from '@/utils/axiosUpload'
 import uploadOss from '@/components/upload';
 import { mapGetters } from 'vuex';
+import { _debounce, _throttle } from '@/utils/auth';
 import axios from 'axios'   // axios 兼容 IE;如不需要可换回 fetch
+import previewFile from './previewFile.vue';
+import moment from 'moment'
+
 
 export default {
     components: {
-        uploadOss
+        uploadOss,
+        previewFile
     },
     model: {
         prop: 'fileUploadVisible',
@@ -34,7 +90,7 @@ export default {
     },
     props: {
         goalId: {
-            type: Number,
+            type: Number | String,
             default: 0
         },
         fileUploadVisible: {
@@ -45,67 +101,162 @@ export default {
             type: Array,
             default: () => []
         },
+        members: {
+            type: Array,
+            default: () => []
+        },
         isAllowUpload: {
             type: Boolean,
             default: true
-        }
+        },
+        isPreview: {
+            type: Boolean,
+            default: false
+        },
+        ownerId: {
+            type: String | Number,
+            default: ''
+        },
     },
     data() {
         return {
+            employeeMap: this.$getEmployeeMap(),
             loading: false,
+            btnLoading: false,
             fileList: [],
             uploadFileList: [],
             imgUrl: '',
+            uploadData: {
+                ...this.$xtoken
+            },
+            initFiles: [],
+            processLength: 0,
+            showProcess: false,
             currentRow: null,
-            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF',
+            acceptFile: '.jpg, .jpeg, .png, .gif, .bmp, .pdf, .JPG, .JPEG, .PBG, .GIF, .BMP, .PDF, .doc, .docx, .xls, .XLSX',
+            currentFile: null,
+            fileViewerDialogVisible: false,
+            config: null,
+            lastAction: "",
+            ossQueue: [], // 1. 排队数组
+            running: false, // 2. 锁
+            batchFiles: [] // 本批次上传成功的文件列表
         }
     },
 
     computed: {
         ...mapGetters(['user_info']),
+        isOwner() {
+            return this.ownerId  === this.user_info.id
+        }
     },
 
     mounted() {
         if (this.fileUploadVisible) {
+            this.initFiles = this.files
             this.uploadFileList = this.files
             this.batchLoad();
-        } 
+        }
     },
 
     methods: {
+
+        parseUrlFile(url) {
+            const [, fileName, ext] = url.match(/\/([^/?#]+)(\.\w+)(?:[?#]|$)/) || [];
+            return { fileName, ext };
+        },
+
+
         onFilePreView(file) {
+            this.currentFile = null;
+            let index = file.name.indexOf("."); 
+            let suffix = file.name.substr(index + 1, file.url.length - 1); //文件后缀名
             let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
-            let lastIndex = file.url && file.url.lastIndexOf("/") || -1
-            let suffix; //文件后缀名
-
-            if (lastIndex > 0) {
-                suffix = file.url.substr(lastIndex + 1, file.url.length - 1).split(".")[1];
-                if (imgFiles.includes(suffix)) {
-                    this.imgUrl = ''
-                    this.imgUrl = file.url;
-                    this.$viewerApi({
-                        images: [this.imgUrl]
-                    })
-                } else {
-                    window.open(file.url, '_blank');
+            console.log("suffix", suffix);
+            if (imgFiles.includes(suffix)) {
+                this.imgUrl = ''
+                this.imgUrl = file.url;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                let currentFile = {
+                    name: file.name,
+                    url: file.url,
+                    type: suffix
                 }
+                this.currentFile = currentFile
+                this.fileViewerDialogVisible = true
+                // window.open(file.url, '_blank');
             }
         },
 
+        onFilePreView2(fileUrl) {
+            this.currentFile = null;
+            let fileObj = this.parseUrlFile(fileUrl);
+            let type = fileObj.ext.replace(".", "")
+            let imgFiles = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg'];
+            if (imgFiles.includes(type)) {
+                this.imgUrl = ''
+                this.imgUrl = fileUrl;
+                this.$viewerApi({
+                    images: [this.imgUrl]
+                })
+            } else {
+                
+                let currentFile = {
+                    name: fileObj.fileName + fileObj.ext,
+                    url: fileUrl,
+                    type
+                }
+                this.currentFile = currentFile;
+                this.fileViewerDialogVisible = true;
+            }
+        },
+
+
+
         handleSuccess: _debounce(function (response, file, fileList) {
             this.uploadFileList = this.fileList.map(item => {
                 return item.url;
             });
 
+            if (this.isAllowUpload) {
+                this.comfirmUploadFiles(this.uploadFileList)
+            } else {
+                this.$emit('close-dialog', false)
+                this.$emit('confirm', this.uploadFileList)
+            }
             // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
         }),
 
+        /* 新增文件(选择后) */
+        handleChange(file, fileList) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                return this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                return this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            
+            this.lastAction = 'add'
+            const url = URL.createObjectURL(file.raw)
+            file.url = url  
+            this.fileList = fileList; // 用来显示的文件列表
+        },
+
         handleRemove(file, fileList) {
+            this.lastAction = 'remove'
+            URL.revokeObjectURL(file.url) // 释放内存
             this.fileList = fileList; // 用来显示的文件列表
             this.uploadFileList = this.fileList.map(item => {
                 return item.url;
             });
-            // this.isAllowUpload && this.comfirmUploadFiles(this.uploadFileList);
         },
 
         beforeFilesUpload(file) {
@@ -123,12 +274,164 @@ export default {
             return isFile && isLt2M;
         },
 
-        
-        
+
+        // 手动上传
+        submitUpload() {
+            this.$refs.upload.submit();
+        },
+
+
+        async get_sign(callback) {
+            let res = await axiosUpload('get', 'https://intesys.cms.g107.com/integral.php/Api/get_signature')
+            this.config = res.data.data
+            await callback()
+        },
+
+        beforeFilesUpload(file) {
+            const $ext_list = ['BMP', 'GIF', 'PNG', 'JPEG', 'JPG', 'bmp', 'gif', 'png', 'jpeg', 'jpg', 'xlsx', 'xls', 'doc', 'docx', 'pdf', 'XLSX', 'XLS', 'DOC', 'DOCX', 'PDF'];
+            const isLt2M = file.size / 1024 / 1024 < 5;
+            let len = file.name.split('.').length - 1;
+            const $ext_name = file.name.split('.')[len];
+            let isFile = $ext_list.indexOf($ext_name) != -1;
+            if (!isLt2M) {
+                this.$message.error('文件大小不能超过 5MB!');
+            }
+            if (!isFile) {
+                this.$message.warning('文件格式上传错误,仅支持上传xlsx,xls,doc,docx,pdf)');
+            }
+            return isFile && isLt2M;
+        },
+
+
+        async oss_upload(upload_obj) {
+            this.ossQueue.push(upload_obj);   // 3. 先扔进队列
+            this.dequeue();                   // 4. 尝试消费
+        },
+
+
+        /* 真正干活的函数,串行,最多一个实例在跑 */
+        async dequeue() {
+            if (this.running || !this.ossQueue.length) return;
+            this.running = true;
+
+            const upload_obj = this.ossQueue.shift();   // 取队首
+            let loading
+            try {
+                this.btnLoading = true;
+                this.loading = true;
+                /* ===== 下面是你原来的整段逻辑,一行没动 ===== */
+                let res = await axiosUpload('get', 'https://intesys.cms.g107.com/integral.php/Api/get_signature');
+                this.config = res.data.data;
+                let item = upload_obj.file;
+                const photoName = item.name;
+                const url = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com';
+                let date = moment().format('YYYY/MM/DD');
+                let site_id = this.coursePath || this.$getCache('site_info').id;
+                let randomStr = this.random_string(32);
+                let key = `intesys/${site_id}/${date}/${randomStr}/${photoName}`;
+                let param = new FormData();
+                param.append('Filename', photoName);
+                param.append('key', key);
+                param.append('policy', this.config.policy);
+                param.append('OSSAccessKeyId', this.config.accessid);
+                param.append('success_action_status', '200');
+                param.append('callback', this.config.callback);
+                param.append('signature', this.config.signature);
+                param.append('file', item);
+                let response = await axios.post(url, param, { headers: { 'Content-Type': 'multipart/form-data' } });
+
+                if (response.data.Status === 'Ok') {
+                    const fileUrl = `https://integralsys.oss-cn-shenzhen.aliyuncs.com/${key}`;
+                    this.fileList.push({
+                        name: randomStr + photoName,
+                        url: fileUrl,
+                        response: { url: fileUrl }
+                    });
+
+                    this.batchFiles.push({ status: 1, url: fileUrl, file_name: randomStr + photoName });
+                    // this.handleSuccess({ status: 1, url: fileUrl, file_name: randomStr + photoName }, item, this.fileList);
+                }
+                /* =========================================== */
+            } catch (e) {
+                console.error('上传失败', e);
+            } finally {
+                this.btnLoading = false;
+                this.loading = false;          // 关闭 loading
+                this.running = false;            // 5. 放锁
+                /* 3. 队列空 → 本批次全部完成 */
+                if (!this.ossQueue.length) {
+                    this.$nextTick(() => {
+                        this.handleSuccess(this.batchFiles); // 一次性给你
+                        this.batchFiles = [];                // 清空,准备下一批
+                    });
+                } else {
+                    this.dequeue(); // 继续下一个
+                }
+                
+            }
+        },
+
+        upload(item) {
+            this.btnLoading = true
+            let self = this
+            const photo = item // 获取图片对象
+            const photoName = item.name // 原图片的名称
+            const url = 'https://integralsys.oss-cn-shenzhen.aliyuncs.com'
+            let date = moment().format('YYYY/MM/DD')
+            let param = new FormData()
+            let site_id
+            if (this.coursePath) {
+                site_id = this.coursePath
+            } else {
+                site_id = this.$getCache('site_info').id
+            }
+            let randomStr = this.random_string(32)
+            let key = 'intesys/' + site_id + '/' + date + '/' + randomStr + '/' + photoName
+            // let loadingInstance = Loading.service({});
+            param.append('Filename', photoName)
+            param.append('key', key)
+            param.append('policy', this.config.policy)
+            param.append('OSSAccessKeyId', this.config.accessid)
+            param.append('success_action_status', '200') // 不要问为什么,照做
+            param.append('callback', this.config.callback)
+            param.append('signature', this.config.signature)
+            param.append('file', photo) // 这个**切记**一定要放到最后去 append ,不然阿里云会一直报 key 的错误
+            axios.post(url, param, {
+                headers: {
+                    'Content-Type': 'multipart/form-data'
+                }
+            }).then(response => {
+                if (response.data.Status == 'Ok') {
+                    self.fileList.push({
+                        name: randomStr + photoName, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, name: item.name, response: {
+                            url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key
+                        }
+                    })
+                    self.handleSuccess({ status: 1, url: 'https://integralsys.oss-cn-shenzhen.aliyuncs.com/' + key, file_name: randomStr + photoName }, item, self.fileList)
+                    this.btnLoading = false
+                }
+            })
+            
+            this.showProcess = false
+
+        },
+
+        random_string(len) {
+            len = len || 32
+            var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
+            var maxPos = chars.length
+            var pwd = ''
+            for (let i = 0; i < len; i++) {
+                pwd += chars.charAt(Math.floor(Math.random() * maxPos))
+            }
+            return pwd
+        },
+
 
         comfirmUploadFiles(files) {
+            this.btnLoading = true
+
             let url = `/okr/og/goals/files/${this.user_info.site_id}`
-            console.log(files);
             let data = {
                 goalId: this.goalId,
                 files
@@ -142,40 +445,69 @@ export default {
                     this.$emit('close-dialog', false)
                     this.$emit('confirm', this.uploadFileList)
                 }
+                this.btnLoading = false
                 this.currentRow = res.data
-                
+
             })
         },
 
-        comfirm() {
-            if (this.isAllowUpload) {
-                this.comfirmUploadFiles(this.uploadFileList)
-            } else {
-                this.$emit('close-dialog', false)
-                this.$emit('confirm', this.uploadFileList)
-            }
-            
+
+        
+
+        cancel() {
+            this.$emit('close-dialog', false)
         },
 
+        comfirm: _throttle(function () {
+            // 检查是否有新文件需要上传
+            const hasNewFiles = this.fileList.some(file => file.url.startsWith("blob"));
+            
+            if (this.lastAction === 'add' || this.lastAction === 'remove') {
+                if (this.lastAction === 'add' || hasNewFiles) {
+                    this.fileList = this.fileList.filter(file => !file.url.startsWith("blob"));
+                    this.submitUpload()
+                } else if (this.lastAction === 'remove')
+                    if (this.isAllowUpload) {
+                        this.comfirmUploadFiles(this.uploadFileList)
+                    } else {
+                        this.$emit('close-dialog', false)
+                        this.$emit('confirm', this.uploadFileList)
+                    }
+                else
+                    this.$emit('close-dialog', false)
+            } else
+                this.$emit('close-dialog', false)
+        }, 300),
+
         dialogBeforeClose() {
             this.$emit('close-dialog', false)
         },
 
+        
+
 
         /* 主函数:接收 url 数组 → 批量回显 */
         async batchLoad() {
             const urlArr = this.files || []; // 已成功上传的文件列表
             if (!urlArr.length) return
-
-            /* 并发下载并转成 File */
-            const taskList = urlArr.map(u => this.url2File(u))
-            try {
-                const files = await Promise.all(taskList)
-                this.fileList = files                // 一次性塞给 el-upload
-                // this.$message.success(`已回显 ${files.length} 个文件`)
-            } catch (e) {
-                this.$message.error('下载失败:' + e.message)
-            }
+            this.fileList = []  // 清空,准备塞入
+            this.loading = true
+            this.fileList = urlArr.map(u => ({
+                name: this.parseUrlFile(u).fileName + this.parseUrlFile(u).ext,
+                url: u,
+                status: 'success'
+            }))
+            this.loading = false
+            // /* 并发下载并转成 File */
+            // const taskList = urlArr.map(u => this.url2File(u))
+            // try {
+            //     const files = await Promise.all(taskList)
+            //     this.fileList = files                // 一次性塞给 el-upload
+            //     console.log('回显文件完成', files)
+            //     // this.$message.success(`已回显 ${files.length} 个文件`)
+            // } catch (e) {
+            //     this.$message.error('下载失败:' + e.message)
+            // }
         },
 
         /* 单条 url → File → el-upload 对象 */
@@ -207,7 +539,54 @@ export default {
                 status: 'success'
             }
         },
+
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.fileViewerDialogBeforeClose();
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        fileViewerDialogBeforeClose() {
+            this.currentFile = null
+            this.fileViewerDialogVisible = false
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
     }
 
+}
+</script>
+
+
+<style scoped lang="scss">
+.dialog-box {
+    width: 100%;
+    max-height: 600px;
+    overflow-y: auto;
+}
+
+.member-item {
+    padding: 10px;
+    box-sizing: border-box;
+    border-bottom: 1px solid #f1f1f1;
+    .employee-name {
+        font-size: 14px;
+        margin-bottom: 10px;
+    }
+    .file-item {
+        margin-bottom: 10px;
     }
-</script>
+}
+</style>

+ 0 - 1
src/okr/views/targetBusiness/components/DataCenter.vue

@@ -214,7 +214,6 @@ export default {
     watch: {
         goalList: {
             handler(newList) {
-                console.log('goalList变化:', newList);
                 this.initializeData();
             },
             immediate: true,

+ 7 - 5
src/okr/views/targetBusiness/components/DataStatistics/StackedBarChart.vue

@@ -225,10 +225,12 @@ export default {
       });
     },
 
+
     handleChildClick(child) {
       this.$emit("viewDetail", child)
     },
 
+
     expandParent(parent) {
       console.log(parent);
     },
@@ -645,7 +647,7 @@ export default {
 .child-list {
   display: flex;
   flex-wrap: wrap;
-  gap: 20px;
+  gap: 40px;
 
   .child-item {
     display: flex;
@@ -677,12 +679,12 @@ export default {
     }
 
     .rate {
-      width: 50px;
-      height: 50px;
+      width: 60px;
+      height: 60px;
       border-radius: 50%;
       text-align: center;
-      line-height: 50px;
-      background-color: #3498db;
+      line-height: 60px;
+      background-color: #007aff;
       margin-bottom: 10px;
       font-size: 14px;
       font-weight: 500;

+ 57 - 7
src/okr/views/targetBusiness/components/MultiCycleTable.vue

@@ -22,7 +22,7 @@
                 <template
                     v-if="rawData.quarterModel && rawData.quarterModel.data && rawData.quarterModel.data.length > 0">
                     <tr class="month-row" v-for="(values, vIndex) in rawData.quarterModel.data" :key="values.code">
-                        <td style="min-width: 180px;">
+                        <td style="min-width: 140px;">
                             <div class="flex-box-ce">
                                 <span v-if="values.code === 'RISK_VALUE'" class="line"
                                     style=" background: #FFEEEB; "></span>
@@ -36,12 +36,17 @@
                                     style=" background: #F3F3FE; "></span>
                                 <span v-if="values.code === 'RESULT_VALUE'" class="line"
                                     style=" background: #c6f7de; "></span>
+                                <span v-if="values.code === 'APPENDS'"></span>
                                 {{ values.name }}
                             </div>
                         </td>
                         <td v-for="(v, index) in values.values" :key="v.valueId" colspan="3">
                             <span v-if="values.code === 'DIFF_VALUE'"
                                 :style="{ color: v.value > 0 ? '#2ecc71' : '#e74c3c' }">{{ v.value }}</span>
+                            <el-link v-else-if="values.code === 'APPENDS' && v.files.length > 0" type="primary"
+                                @click="openFileUpload(v)">
+                                <i class="el-icon-view"></i>
+                            </el-link>
                             <span v-else>{{ v.value }}</span>
                         </td>
                     </tr>
@@ -69,7 +74,7 @@
 
                 <template v-if="rawData.monthModel && rawData.monthModel.data && rawData.monthModel.data.length > 0">
                     <tr class="month-row" v-for="(values, vIndex) in rawData.monthModel.data" :key="vIndex">
-                        <td style="min-width: 180px;">
+                        <td style="min-width: 140px;">
                             <div class="flex-box-ce">
                                 <span v-if="values.code === 'RISK_VALUE'" class="line"
                                     style=" background: #FFEEEB; "></span>
@@ -83,12 +88,17 @@
                                     style=" background: #F3F3FE; "></span>
                                 <span v-if="values.code === 'RESULT_VALUE'" class="line"
                                     style=" background: #c6f7de; "></span>
+                                <span v-if="values.code === 'APPENDS'"></span>
                                 {{ values.name }}
                             </div>
                         </td>
                         <td v-for="(v, index) in values.values" :key="v.valueId">
                             <span v-if="values.code === 'DIFF_VALUE'"
                                 :style="{ color: v.value > 0 ? '#2ecc71' : '#e74c3c' }">{{ v.value }}</span>
+                            <el-link v-else-if="values.code === 'APPENDS' && v.files.length > 0" type="primary"
+                                @click="openFileUpload(v)">
+                                <i class="el-icon-view"></i>
+                            </el-link>
                             <span v-else>{{ v.value }}</span>
                         </td>
                     </tr>
@@ -114,7 +124,7 @@
 
                 <template v-if="rawData.dayModel && rawData.dayModel.data && rawData.dayModel.data.length > 0">
                     <tr class="month-row" v-for="(values, vIndex) in rawData.dayModel.data" :key="vIndex">
-                        <td style="min-width: 180px;">
+                        <td style="min-width: 140px;">
                             <div class="flex-box-ce">
                                 <span v-if="values.code === 'RISK_VALUE'" class="line"
                                     style=" background: #FFEEEB; "></span>
@@ -128,6 +138,7 @@
                                     style=" background: #F3F3FE; "></span>
                                 <span v-if="values.code === 'RESULT_VALUE'" class="line"
                                     style=" background: #c6f7de; "></span>
+                                <span v-if="values.code === 'APPENDS'"></span>
                                 {{ values.name }}
                             </div>
                         </td>
@@ -135,6 +146,10 @@
                             <span v-if="values.code === 'DIFF_VALUE'"
                                 :style="{ color: v.value > 0 || v.value == 0 ? '#2ecc71' : '#e74c3c' }">{{ v.value
                                 }}</span>
+                            <el-link v-else-if="values.code === 'APPENDS' && v.files.length > 0" type="primary"
+                                @click="openFileUpload(v)">
+                                <i class="el-icon-view"></i>
+                            </el-link>
                             <span v-else>{{ v.value }}</span>
                         </td>
                     </tr>
@@ -158,17 +173,34 @@
         <template v-else>
             <div class="no-data fontColorC">暂无数据,快去拆解目标吧</div>
         </template>
+
+        <!-- <el-dialog title="文件预览" :visible.sync="fileListVisible" width="600px" :before-close="dialogBeforeClose">
+            <div v-loading="loading">
+                <div v-for="(file, index) in fileList" :key="index" @click="onFilePreView(file)">{{ file }}</div>
+            </div>
+        </el-dialog>
+
+        <el-dialog title="文件预览" :visible.sync="fileViewerDialogVisible" fullscreen append-to-body @open="onDialogOpen"
+            :before-close="fileViewerDialogBeforeClose">
+            <previewFile v-if="fileViewerDialogVisible" :file="currentFile" />
+        </el-dialog> -->
+
+        <!-- 文件上传/预览组件 -->
+        <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="targetDetail.goalId"
+            :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
     </div>
 </template>
 
 
 <script>
+import { mapGetters } from 'vuex';
 import CardList from './CardList'; // 卡片列表
-
+import AppendFileUpload from './AppendFileUpload.vue';
 export default {
     name: 'NativeHierarchicalTable',
     components: {
-        CardList
+        CardList,
+        AppendFileUpload
     },
     props: {
         isCardListShow: {
@@ -204,6 +236,7 @@ export default {
     },
 
     computed: {
+        ...mapGetters(['user_info']),
         // 是否有季度数据
         isHaveQuarterModelData() {
             return this.rawData.quarterModel && this.rawData.quarterModel.scopes && this.rawData.quarterModel.scopes.length > 0
@@ -295,7 +328,11 @@ export default {
         return {
             targetDetail: {
                 quantifyModels: []
-            }
+            },
+            loading: false,
+            imgUrl: "",
+            files: [],
+            fileUploadVisible: false,
         }
     },
 
@@ -336,6 +373,19 @@ export default {
     },
     methods: {
 
+        confirmAppendFile(files) {},
+
+        openFileUpload(v) {
+            let { members, files } = v
+            this.members = members
+            this.files = files
+            this.fileUploadVisible = true
+            // this.files = files;
+            // this.fileUploadVisible = true;
+        },
+
+
+       
         /**
          * 格式化数值显示
          */
@@ -413,7 +463,7 @@ export default {
 
 /* 时间周期列样式 */
 .period-th {
-    width: 180px;
+    width: 100px;
     text-align: left !important;
     padding-left: 20px !important;
 }

+ 5 - 4
src/okr/views/targetBusiness/components/TargetDetail/EditBaseTableData.vue

@@ -92,7 +92,7 @@
                         </template>
                     </vxe-table-column>
 
-                    <vxe-table-column field="type" title="类型" min-width="100">
+                    <vxe-table-column field="type" title="类型" min-width="200">
                         <template #default="{ row }">
                             <el-tag v-if="row.type == 1" type="primary">公司目标</el-tag>
                             <el-tag v-if="row.type == 2" type="primary">{{ row.deptName }}</el-tag>
@@ -211,13 +211,14 @@
                 :employee_list="$getEmployeeMap(1).filter(item => item.account_id !== 0)"
                 :visible.sync="show_employee_search" @confirm="employee_confirm" />
 
-
             <!-- 上传附件 -->
             <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="currentRow.goalId"
-                :files="files" @confirm="confirmAppendFile" />
+                :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
 
+            <!-- 选择上级目标 -->
             <SelectSeniorTargetDialog :visible.sync="chooseSeniorDialogVisible" :selected="currentRow"
                 :type="currentTargetType" :scopeId="scopeId" @confirm="confirmChooseSeniorTarget" />
+
         </div>
         <div slot="footer">
             <el-button type="primary" @click="confirmSave()" size="small">批量进入制定阶段</el-button>
@@ -387,6 +388,7 @@ export default {
                 }
                 obj['goalId'] = this.targetDetail.goalId
                 obj['name'] = this.targetDetail.name
+                obj['deptName'] = this.targetDetail.deptName
                 obj['ownerId'] = this.targetDetail.ownerId
                 obj['standard'] = this.targetDetail.standard
                 obj['status'] = this.targetDetail.status
@@ -452,7 +454,6 @@ export default {
             }
 
             this.$http.post(url, data).then(res => {
-                console.log(res);
                 if (res.code == 1) {
                     this.tableData.splice(this.currentRowIndex, 1, res.data)
                     this.$message.success('操作成功')

+ 4 - 7
src/okr/views/targetBusiness/components/TargetDetail/EditTableData.vue

@@ -8,8 +8,9 @@
                 <el-button type="primary" size="mini" plain round @click="svgTargetNums()">一键平均分配</el-button>
             </div>
 
-            <el-button :disabled="isDisabled" type="primary" size="small" plain round @click="handleSave()">保
-                存</el-button>
+            <el-button :disabled="isDisabled" type="primary" size="small" plain round @click="handleSave()">
+                保 存
+            </el-button>
         </div>
 
         <!-- 提示条 -->
@@ -284,7 +285,7 @@
 
         <!-- 上传附件 -->
         <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :is-allow-upload="false"
-            :goalId="currentRow.goalId" :files="files" @confirm="confirmAppendFile" />
+            :goalId="currentRow.goalId" :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
 
 
     </div>
@@ -690,9 +691,7 @@ export default {
             row.visibleType = this.visibleType;
             row.specialEmployeeIds = this.visibleEmployeeSelectedObj.employee || [];
             row.specialDeptIds = this.dept_selected.dept || [];
-            console.log(row);
             this.tableData.splice(this.currentRowIndex, 1, row)
-            console.log(this.tableData);
             this.dept_selected = { dept: [], employee: [] };
             this.employee_selected = { dept: [], employee: [] };
         },
@@ -886,7 +885,6 @@ export default {
         },
 
         add_target_employee_confirm(data) {
-            console.log(data)
             this.add_target_employee_selected = { dept: [], employee: [] };
             if (data.employee !== null && data.employee.length != 0) {
                 this.add_target_employee_selected = data;
@@ -1017,7 +1015,6 @@ export default {
             // 预估达成值 - 目标数
             let projectValueObj = this.targetDetail.quantifyModels.find(item => item.name === '预估达成值')
             let projectValueTarget = projectValueObj.autoBySub ? projectValueObj.subValue : projectValueObj.value
-            // console.log(projectValueObj)
             // 如果目标数为0,返回一个全是0的数组
             let zeroArray = Array.from({ length: range }, (v, i) => 0)
             // 平分红线值

+ 0 - 1
src/okr/views/targetBusiness/components/TargetDetail/MessageList.vue

@@ -144,7 +144,6 @@ export default {
                 })
                 this.total = res.data.data.total;
                 this.scrollTopZero(); // 滚动到顶部
-                console.log(this.messageList)
                 
             })
         },

+ 0 - 1
src/okr/views/targetBusiness/components/TargetDetail/TargetSearchMulti.vue

@@ -472,7 +472,6 @@ export default {
             }
           })
           this.targetList = list;
-          console.log(this.tartgetList);
           this.total = res.data.data.total;
         }
       })

+ 0 - 3
src/okr/views/targetBusiness/components/TargetDetail/TargetSearchSingle.vue

@@ -294,9 +294,6 @@ export default {
           break
         }
       }
-      console.log("子",this.checkList);
-      console.log("父",this.parentCheckList);
-      console.log("选择的数据",this.selectedData);
       
     },
 

+ 0 - 1
src/okr/views/targetBusiness/components/TargetDetail/TaskList.vue

@@ -115,7 +115,6 @@ export default {
                         this.taskShow = true;
                     }
                 }, 100)
-                console.log(res)
             })
         },
         updateTaskList() {

+ 252 - 25
src/okr/views/targetBusiness/components/TargetDetailComp.vue

@@ -40,7 +40,7 @@
                                     style="margin: 0 5px 0 0;"></svg-icon>
                                 负责人: {{ ownerName }}
                             </div>
-                            
+
                             <div class="tag" v-if="targetDetail.scope.cycleType == 1"> <i
                                     class="el-icon-alarm-clock"></i> 年度 {{ targetDetail.scope.name }}</div>
                             <div class="tag" v-if="targetDetail.scope.cycleType == 2"> <i
@@ -143,6 +143,8 @@
                                         </div>
                                         <div v-if="row.code === 'DIFF_VALUE'" class="line">
                                         </div>
+                                        <div v-if="row.code === 'APPENDS' && isAppendShow">
+                                        </div>
                                         <div>{{ row.name }}</div>
                                     </div>
                                 </template>
@@ -157,8 +159,34 @@
                                         style: { textAlign: 'left' }
                                     }">
                                     <template #default="{ row, rowIndex }">
-                                        <template v-for="value in row.values">
-                                            <span v-if="value.scopeId == header.scopeId">{{ value.value }}</span>
+                                        <template v-for="(value, index) in row.values">
+
+                                            <div v-if="value.scopeId == header.scopeId">
+
+                                                <div v-if="value.scopeId == header.scopeId">
+                                                    <template v-if="isAppendShow && row.code === 'APPENDS'">
+                                                        <div class="flex-box-ce"  style="justify-content: space-between;">
+                                                            <div
+                                                                v-if="row.values[index].files && row.values[index].files.length > 0">
+                                                                <el-tooltip effect="dark" content="预览附件"
+                                                                    placement="top">
+                                                                    <el-link type="primary"
+                                                                        @click="openFileUpload(row, index, 'preview')">
+                                                                        <i class="el-icon-view"></i>
+                                                                    </el-link>
+                                                                </el-tooltip>
+                                                            </div>
+                                                        </div>
+                                                    </template>
+
+                                                    <span v-else
+                                                        :class="[row.allowEdit ? 'cursor-pointer' : '', row.code === 'DIFF_VALUE' ? value.value > 0 ? 'color-green' : 'color-red' : '']">{{
+                                                        value.value }}
+                                                    </span>
+                                                </div>
+
+
+                                            </div>
                                         </template>
                                     </template>
                                 </vxe-column>
@@ -189,7 +217,10 @@
             </div>
         </el-dialog>
 
-        
+        <!-- 文件上传/预览组件 -->
+        <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="goalId" :files="files"
+            :members="members" :isAllowUpload="false" :isPreview="isPreview" :ownerId="targetDetail.ownerId"
+            @confirm="confirmAppendFile" />
     </div>
 </template>
 
@@ -206,7 +237,7 @@ import TaskList from './TargetDetail/TaskList.vue';
 import MessageList from './TargetDetail/MessageList.vue';
 import EditAutoSubSwitch from "./TargetDetail/EditAutoSubSwitch.vue"
 import DataCenter from './DataCenter.vue'
-
+import AppendFileUpload from './AppendFileUpload.vue';
 export default {
     components: {
         GaugeChart,
@@ -217,7 +248,8 @@ export default {
         TaskList,
         MessageList,
         MultiCycleTable,
-        DataCenter
+        DataCenter,
+        AppendFileUpload
     },
     props: {
         goalId: [String, Number]
@@ -263,7 +295,13 @@ export default {
                 type: 1,
                 unit: ""
             },
-
+            members: [],
+            files: [],
+            isPreview: false,
+            fileUploadVisible: false,
+            currentRow: null,
+            cellIndex: 0,
+            valueId: '',
             autoBySub: false, // 预计达成值, 完成值是否与子目标决定
             rawData: {},
             title: "",
@@ -283,6 +321,7 @@ export default {
                 }
             }
         },
+
         autoBySub(oVal, nVal) {
             if (!nVal) {
                 let url = `/okr/og/goals/plan/statistics/${this.user_info.site_id}/${this.targetDetail.goalId}`
@@ -293,39 +332,98 @@ export default {
                         name: "差值(目标值 - 完成值)",
                         values: []
                     }
+
+                    let append = {
+                        code: "APPENDS",
+                        name: "附件",
+                        values: []
+                    }
+
                     if (this.isHaveQuarterModelData) {
                         let targetValues = this.rawData.quarterModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                         let resultValues = this.rawData.quarterModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                         targetValues.forEach((value, index) => {
-                            obj.values.push({ value: resultValues[index] - targetValues[index] })
+                            let result = resultValues[index] - targetValues[index]
+                            obj.values.push({ scopeId: value.scopeId, value: Number.isInteger(result) ? result : result.toFixed(2) })
                         })
                         this.rawData.quarterModel.data.push(obj)
+
+                        this.rawData.quarterModel.data.forEach(item => {
+                            if (item.code === 'RESULT_VALUE') {
+                                item.values.forEach(value => {
+                                    let members = value.members, files = [];
+                                    if (members && members.length > 0) {
+                                        members.forEach(member => {
+                                            files = member.files
+                                        })
+                                    } else
+                                        files = []
+                                    append.values.push({ files, members })
+                                })
+                            }
+                        })
+
+                        this.rawData.quarterModel.data.push(append)
+
                         obj = {
                             code: "DIFF_VALUE",
                             name: "差值(目标值 - 完成值)",
                             values: []
                         }
+
+                        append = {
+                            code: "APPENDS",
+                            name: "附件",
+                            values: []
+                        }
                     }
 
                     if (this.isHaveMonthModelData) {
                         let targetValues = this.rawData.monthModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                         let resultValues = this.rawData.monthModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                         targetValues.forEach((value, index) => {
-                            obj.values.push({ value: resultValues[index] - targetValues[index] })
+                            let result = resultValues[index] - targetValues[index]
+                            obj.values.push({ value: Number.isInteger(result) ? result : result.toFixed(2) })
                         })
+
                         this.rawData.monthModel.data.push(obj)
+
+                        this.rawData.monthModel.data.forEach(item => {
+                            if (item.code === 'RESULT_VALUE') {
+                                item.values.forEach(value => {
+                                    let members = value.members, files = [];
+                                    if (members && members.length > 0) {
+                                        members.forEach(member => {
+                                            files = member.files
+                                        })
+                                    } else
+                                        files = []
+                                    append.values.push({ files, members })
+                                })
+                            }
+                        })
+
+                        this.rawData.monthModel.data.push(append)
+
                         obj = {
                             code: "DIFF_VALUE",
                             name: "差值(目标值 - 完成值)",
                             values: []
                         }
+
+                        append = {
+                            code: "APPENDS",
+                            name: "附件",
+                            values: []
+                        }
                     }
 
                     if (this.isHaveDayModelData) {
                         let targetValues = this.rawData.dayModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                         let resultValues = this.rawData.dayModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                         targetValues.forEach((value, index) => {
-                            obj.values.push({ value: resultValues[index] - targetValues[index] })
+                            let result = resultValues[index] - targetValues[index]
+                            obj.values.push({ scopeId: value.scopeId, value: Number.isInteger(result) ? result : result.toFixed(2) })
                         })
                         this.rawData.dayModel.data.push(obj)
                         obj = {
@@ -338,8 +436,33 @@ export default {
                             if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
                                 finalData.push(item)
                             }
+                            if (item.code === 'RESULT_VALUE') {
+                                item.values.forEach(value => {
+                                    let members = value.members, files = [];
+                                    if (members && members.length > 0) {
+                                        members.forEach(member => {
+                                            files = member.files
+                                        })
+                                    } else
+                                        files = []
+                                    append.values.push({ files, members })
+                                })
+                            }
                         })
-                        this.rawData.dayModel.data = finalData 
+
+                        finalData.push(append)
+                        this.rawData.dayModel.data = finalData
+                        obj = {
+                            code: "DIFF_VALUE",
+                            name: "差值(目标值 - 完成值)",
+                            values: []
+                        }
+
+                        append = {
+                            code: "APPENDS",
+                            name: "附件",
+                            values: []
+                        }
                     }
                 })
 
@@ -350,6 +473,17 @@ export default {
 
     computed: {
         ...mapGetters(['user_info']),
+
+        // 是否可以操作附件
+        isAppendShow() {
+            return this.targetDetail.status == 2
+        },
+
+        // 负责人可以操作附件
+        isAllowAppend() {
+            return this.user_info.id == this.targetDetail.ownerId
+        },
+
         ownerName() {
             return this.employeeMap[this.targetDetail.ownerId] && this.employeeMap[this.targetDetail.ownerId].name || '--'
         },
@@ -388,8 +522,18 @@ export default {
             let quantifyModels = this.targetDetail && this.targetDetail.quantifyModels || []
             if (quantifyModels.length == 0) return 0
             let target = quantifyModels.find(item => item.code == 'TARGET_VALUE').value // 目标值
-            let result = quantifyModels.find(item => item.code == 'RESULT_VALUE').autoBySub ? quantifyModels.find(item => item.code == 'RESULT_VALUE').subValue : quantifyModels.find(item => item.code == 'RESULT_VALUE').value // 完成值
-            return result ? Number(result / target) * 100 : 0
+            let autoBySub = quantifyModels.find(item => item.code == 'RESULT_VALUE').autoBySub
+            let subValue = quantifyModels.find(item => item.code == 'RESULT_VALUE').subValue
+            let value = quantifyModels.find(item => item.code == 'RESULT_VALUE').value
+            let result = autoBySub ? subValue : value // 完成值
+            if (target == 0 || result == 0) return 0
+            // let calcPercent = Number.isNaN(Number(result / target) * 100) || !Number.isFinite(Number(result / target) * 100) ? 0 : Number(result / target) * 100
+            else return Number.isNaN(Number(result / target) * 100) || !Number.isFinite(Number(result / target) * 100) ? 0 : Number(result / target) * 100
+        },
+
+        // 是否可以操作附件
+        isAppendShow() {
+            return this.targetDetail.status == 2
         },
 
         // 是否有季度数据
@@ -432,15 +576,12 @@ export default {
             this.initEditStatus()
         },
 
-
-
         async getDetails() {
             this.loading = true
             let url = `/okr/og/goals/info/${this.user_info.site_id}/${this.goalId}`;
             let res = await this.$axiosUser('get', url)
             this.loading = false
             this.targetDetail = res.data.data
-            // console.log(this.targetDetail.);
             let quantifyModels = this.targetDetail && this.targetDetail.quantifyModels || []
             let { autoBySub } = quantifyModels.find(item => item.code === 'PROJECTED_VALUE')
             this.autoBySub = autoBySub;
@@ -451,26 +592,76 @@ export default {
                     name: "差值(目标值 - 完成值)",
                     values: []
                 }
+
+                let append = {
+                    code: "APPENDS",
+                    name: "附件",
+                    values: []
+
+                }
+
                 let targetValues = this.targetDetail.planModels.data.find(item => item.code === "TARGET_VALUE").values.map(value => value)
                 let resultValues = this.targetDetail.planModels.data.find(item => item.code === "RESULT_VALUE").values.map(value => value)
                 targetValues.forEach((value, index) => {
-                    obj.values.push({ scopeId: value.scopeId, value: resultValues[index].value - targetValues[index].value })
+                    let result = resultValues[index].value - targetValues[index].value
+                    obj.values.push({ scopeId: value.scopeId, value: Number.isInteger(result) ? result : result.toFixed(2) })
                 })
                 this.targetDetail.planModels.data.push(obj)
+
+                resultValues.forEach((value, index) => {
+                    let members = value.members, files = []
+                    if (members && members.length > 0) {
+                        members.forEach(member => {
+                            files = member.files
+                        })
+                    } else
+                        files = []
+                    append.values.push({ files, members, scopeId: value.scopeId, valueId: value.valueId, })
+                })
+                this.targetDetail.planModels.data.push(append)
             }
 
             
-            if (this.targetDetail && this.targetDetail.planModels && this.targetDetail.planModels.data && this.targetDetail.planModels.data[0].values.length > 12) {
-                let finalData = [];
-                this.targetDetail.planModels.data.forEach(item => {
-                    if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
-                        finalData.push(item)
+            if (this.targetDetail.scope.cycleType == 4) {
+                if (this.targetDetail && this.targetDetail.planModels && this.targetDetail.planModels.data && this.targetDetail.planModels.data.length > 0) {
+
+                    let finalData = [];
+                    this.targetDetail.planModels.data.forEach(item => {
+                        if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
+                            finalData.push(item)
+                        }
+                    })
+                    this.targetDetail.planModels.data = finalData
+
+                    let append = {
+                        code: "APPENDS",
+                        name: "附件",
+                        values: []
                     }
-                })
-                this.targetDetail.planModels.data = finalData
+
+                    this.targetDetail.planModels.data.forEach(item => {
+                        if (item.code === 'RESULT_VALUE') {
+                            item.values.forEach(value => {
+                                let members = value.members, files = []
+                                if (members && members.length > 0) {
+                                    members.forEach(member => {
+                                        files = member.files
+                                    })
+                                } else
+                                    files = []
+                                append.values.push({ files, members, scopeId: value.scopeId, valueId: value.valueId, })
+                            })
+
+                        }
+                    })
+
+                    this.targetDetail.planModels.data.push(append)
+
+
+
+                }
 
             }
-            
             const scopes = this.targetDetail.planModels.scopes;
             const data = this.flattenRowValues(this.targetDetail.planModels.data, scopes);
         },
@@ -491,6 +682,33 @@ export default {
             this.dataCenterDialogVisible = true
         },
 
+        openFileUpload(row, index, operation) {
+            if (row.status == 2 || row.status == 3) return this.$message.error("目标正在执行中或已结束,不允许修改")
+            if (operation == 'preview') this.isPreview = true
+            else this.isPreview = false
+            this.currentRow = row
+            this.cellIndex = index
+            let { members, files } = row.values[index]
+            this.members = members
+            this.files = files
+            this.fileUploadVisible = true
+        },
+
+        confirmAppendFile(files) {
+            let url = `/okr/og/goals/plan/result/files/${this.user_info.site_id}`
+            let data = {
+                goalId: this.goalId,
+                valueId: this.currentRow.values[this.cellIndex].valueId,
+                files
+            }
+            this.$http.post(url, data).then(res => {
+                if (res.data.code = 1)
+                    this.getDetails();
+                else
+                    this.$message.error(res.message || '操作失败')
+            })
+        },
+
         /* 拦截浏览器返回键 */
         blockPopstate(e) {
             // 阻止默认返回行为
@@ -523,6 +741,15 @@ export default {
 </script>
 
 <style lang="scss">
+
+.color-green {
+    color: #2ecc71;
+}
+
+.color-red {
+    color: #e74c3c;
+}
+
 /* 让标题与按钮在同一行,按钮靠右 */
 .alert-title {
     display: flex;

+ 1256 - 0
src/okr/views/targetBusiness/components/previewFile.vue

@@ -0,0 +1,1256 @@
+<template>
+    <div class="file-preview-container">
+        <div class="preview-area">
+            <div class="preview-header">
+                <h3>{{ currentFile.name || '文件预览' }}</h3>
+                <div class="file-actions" v-if="currentFile.id || currentFile.uid">
+                    <button class="action-btn" @click="downloadFile">
+                        <span class="action-icon">⬇️</span>
+                        下载
+                    </button>
+                </div>
+            </div>
+
+            <div class="preview-container">
+                <div v-if="!currentFile" class="preview-content">
+                    <div class="preview-placeholder">
+                        <div class="placeholder-icon">📄</div>
+                        <h3>暂无文件</h3>
+                        <p>请传入文件数据进行预览</p>
+                    </div>
+                </div>
+
+                <div v-else class="preview-content">
+                    <!-- 加载状态 -->
+                    <div v-if="loading" class="loading">
+                        <div class="spinner"></div>
+                        <p>加载中...</p>
+                        <p v-if="loadProgress > 0">加载进度: {{ loadProgress }}%</p>
+                    </div>
+
+                    <!-- PDF预览 -->
+                    <div v-else-if="currentFile.type === 'pdf'" class="pdf-preview">
+                        <div class="error-message" v-if="pdfError">
+                            <h3>PDF加载失败</h3>
+                            <p>{{ pdfError }}</p>
+                            <button @click="retryLoadPdf"
+                                style="margin-top: 10px; padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;">
+                                重试
+                            </button>
+                        </div>
+
+                        <div v-else>
+                            <div class="pdf-controls">
+                                <button @click="prevPage" :disabled="pdfPageNum <= 1">
+                                    <span>◀</span> 上一页
+                                </button>
+                                <span class="page-info">
+                                    第 {{ pdfPageNum }} 页 / 共 {{ pdfTotalPages }} 页
+                                </span>
+                                <button @click="nextPage" :disabled="pdfPageNum >= pdfTotalPages">
+                                    下一页 <span>▶</span>
+                                </button>
+                                <select v-model="pdfScale" @change="handleScaleChange" class="scale-select">
+                                    <option value="0.5">50%</option>
+                                    <option value="0.75">75%</option>
+                                    <option value="1">100%</option>
+                                    <option value="1.25">125%</option>
+                                    <option value="1.5">150%</option>
+                                    <option value="2">200%</option>
+                                </select>
+                                <button @click="fitToWidth">适应宽度</button>
+                            </div>
+                            <div class="pdf-viewer">
+                                <canvas ref="pdfCanvas"></canvas>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- 图片预览 -->
+                    <div v-else-if="currentFile.type === 'image'" class="image-preview-container">
+                        <img :src="currentFile.url" :alt="currentFile.name" class="image-preview"
+                            :style="{ transform: `scale(${imageScale})` }">
+                        <div class="image-actions">
+                            <button @click="zoomIn" :disabled="imageScale >= 3">放大</button>
+                            <button @click="zoomOut" :disabled="imageScale <= 0.5">缩小</button>
+                            <button @click="resetZoom">重置</button>
+                            <span class="zoom-level">{{ Math.round(imageScale * 100) }}%</span>
+                        </div>
+                    </div>
+
+                    <!-- Excel预览 -->
+                    <div v-else-if="currentFile.type === 'excel'" class="excel-preview">
+                        <!-- Sheet 切换 -->
+                        <div class="sheet-tabs" v-if="sheets.length">
+                            <div class="tabs-container">
+                                <div v-for="s in sheets" :key="s.name"
+                                    :class="['tab-item', { active: pickedSheet === s.name }]"
+                                    @click="changeSheet(s.name)">
+                                    <span class="tab-name">{{ s.name }}</span>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div v-if="tableData.length" class="excel-panel">
+                            <!-- 表格信息栏 -->
+                            <div class="excel-info">
+                                <div class="info-item">
+                                    <span class="label">当前工作表:</span>
+                                    <span class="value">{{ pickedSheet }}</span>
+                                </div>
+                                <div class="info-item">
+                                    <span class="label">数据行数:</span>
+                                    <span class="value">{{ tableData.length }}</span>
+                                </div>
+                                <div class="info-item">
+                                    <span class="label">列数:</span>
+                                    <span class="value">{{ tableData[0] ? tableData[0].length : 0 }}</span>
+                                </div>
+                            </div>
+
+                            <!-- 表格容器 -->
+                            <div class="table-container">
+                                <!-- 顶部横向滚动条 -->
+                                <div class="horizontal-scrollbar" ref="horizontalScrollbar">
+                                    <div class="scrollbar-track" ref="scrollbarTrack"></div>
+                                </div>
+
+                                <!-- 表格区域 -->
+                                <div class="table-wrapper" ref="tableWrapper">
+                                    <table class="excel-table" ref="excelTable">
+                                        <tbody>
+                                            <tr v-for="(row, r) in tableData" :key="r"
+                                                :class="{ 'header-row': r === 0 }">
+                                                <td v-for="(cell, c) in row" :key="c" :rowspan="getRowspan(r, c)"
+                                                    :colspan="getColspan(r, c)" v-if="shouldShowCell(r, c)" :class="{
+                                                        'header-cell': r === 0,
+                                                        'data-cell': r > 0
+                                                    }">
+                                                    <div class="cell-content">
+                                                        {{ cell || '' }}
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 空状态 -->
+                        <div v-else class="empty-state">
+                            <div class="empty-icon">📊</div>
+                            <h3>暂无数据</h3>
+                            <p>当前工作表没有数据</p>
+                        </div>
+                    </div>
+
+                    <!-- Word预览 -->
+                    <div v-else-if="currentFile.type === 'word'" class="word-preview">
+                        <div class="word-content" v-if="wordContent" v-html="wordContent"></div>
+                        <div v-else class="no-content">
+                            无法预览此Word文档
+                        </div>
+                    </div>
+
+                    <!-- 不支持的文件类型 -->
+                    <div v-else class="unsupported-file">
+                        <div class="unsupported-icon">❌</div>
+                        <h3>不支持的文件格式</h3>
+                        <p>当前文件格式无法预览,请下载后查看</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+// 引入必要的库
+import * as XLSX from 'xlsx'
+
+// 设置PDF.js worker路径 - 使用稳定版本的Worker
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
+
+export default {
+    name: "FilePreview",
+    props: {
+        // 文件对象,格式为 {name, url, raw, status, uid}
+        file: {
+            type: Object,
+            default: () => ({})
+        }
+    },
+    data() {
+        return {
+            currentFile: {},
+            loading: false,
+            loadProgress: 0,
+
+            // PDF相关
+            pdfPageNum: 1,
+            pdfTotalPages: 0,
+            pdfDoc: null,
+            pdfScale: 1.5,
+            pdfError: '',
+            scaleMode: 'scale', // 'scale' 或 'width'
+
+            // 图片相关
+            imageScale: 1,
+
+            // Excel相关
+            workbook: null,
+            sheets: [],
+            pickedSheet: '',
+            tableData: [],
+            mergeMap: {},
+            originalMerges: [], // 存储原始合并信息
+
+            // Word相关
+            wordContent: '',
+        };
+    },
+
+    watch: {
+        // 监听file prop变化
+        file: {
+            handler(newFile) {
+                if (newFile && newFile.url) {
+                    this.selectFile(newFile);
+                }
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+
+    methods: {
+        // 根据文件扩展名确定类型
+        getFileType(file) {
+            const name = file.name.toLowerCase();
+            if (name.endsWith('.pdf')) return 'pdf';
+            if (name.endsWith('.xlsx') || name.endsWith('.xls')) return 'excel';
+            if (name.endsWith('.docx') || name.endsWith('.doc')) return 'word';
+            if (name.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/)) return 'image';
+            return 'unknown';
+        },
+
+        // 选择文件
+        async selectFile(file) {
+            this.currentFile = file;
+            this.loading = true;
+            this.loadProgress = 0;
+
+            // 确定文件类型
+            this.currentFile.type = this.getFileType(file);
+            // 重置状态
+            this.pdfPageNum = 1;
+            this.pdfTotalPages = 0;
+            this.pdfDoc = null;
+            this.pdfError = '';
+            this.imageScale = 1;
+            this.wordContent = '';
+
+            // 重置Excel状态
+            this.workbook = null;
+            this.sheets = [];
+            this.pickedSheet = '';
+            this.tableData = [];
+            this.mergeMap = {};
+            this.originalMerges = [];
+            try {
+                switch (this.currentFile.type) {
+                    case 'pdf':
+                        await this.loadPdf(this.currentFile.url);
+                        break;
+                    case 'excel':
+                        await this.loadExcel(this.currentFile.url);
+                        break;
+                    case 'word':
+                        await this.loadWord(this.currentFile.url);
+                        break;
+                    case 'image':
+                        // 图片直接通过URL显示,无需特殊处理
+                        break;
+                    default:
+                        console.warn('不支持的文件类型:', this.currentFile.type);
+                }
+            } catch (error) {
+                console.error('文件加载失败:', error);
+                this.pdfError = this.getPdfErrorMessage(error);
+            } finally {
+                this.loading = false;
+            }
+        },
+
+        // 加载PDF
+        async loadPdf(url) {
+            try {
+                // 创建加载任务
+                const loadingTask = pdfjsLib.getDocument({
+                    url: url,
+                    // 添加进度回调
+                    progressCallback: (progress) => {
+                        this.loadProgress = Math.round((progress.loaded / progress.total) * 100);
+                    }
+                });
+
+                // 加载PDF文档
+                this.pdfDoc = await loadingTask.promise;
+                this.pdfTotalPages = this.pdfDoc.numPages;
+
+                // 使用$nextTick确保canvas已经渲染
+                this.$nextTick(() => {
+                    this.renderPdfPage(this.pdfPageNum);
+                });
+
+            } catch (error) {
+                console.error('PDF加载失败:', error);
+                this.pdfError = this.getPdfErrorMessage(error);
+                throw error;
+            }
+        },
+
+        // 渲染PDF页面
+        async renderPdfPage(pageNum) {
+            try {
+                if (!this.pdfDoc) return;
+
+                const page = await this.pdfDoc.getPage(pageNum);
+                const canvas = this.$refs.pdfCanvas;
+                if (!canvas) {
+                    console.error('Canvas element not found');
+                    return;
+                }
+                const ctx = canvas.getContext('2d');
+
+                // 获取原始视图
+                const viewport = page.getViewport({ scale: 1 });
+
+                let scale;
+                if (this.scaleMode === 'width') {
+                    // 适应宽度
+                    const container = document.querySelector('.pdf-viewer');
+                    const containerWidth = container ? container.clientWidth - 40 : 800;
+                    scale = containerWidth / viewport.width;
+                } else {
+                    // 使用选择的缩放比例
+                    scale = parseFloat(this.pdfScale);
+                }
+
+                // 应用缩放
+                const scaledViewport = page.getViewport({ scale: scale });
+
+                // 设置Canvas尺寸
+                canvas.height = scaledViewport.height;
+                canvas.width = scaledViewport.width;
+
+                // 渲染配置
+                const renderContext = {
+                    canvasContext: ctx,
+                    viewport: scaledViewport
+                };
+
+                // 渲染页面
+                await page.render(renderContext).promise;
+
+            } catch (error) {
+                console.error('PDF渲染失败:', error);
+                this.pdfError = '页面渲染失败: ' + error.message;
+                throw error;
+            }
+        },
+
+        // PDF上一页
+        async prevPage() {
+            if (this.pdfPageNum > 1) {
+                this.pdfPageNum--;
+                await this.renderPdfPage(this.pdfPageNum);
+            }
+        },
+
+        // PDF下一页
+        async nextPage() {
+            if (this.pdfPageNum < this.pdfTotalPages) {
+                this.pdfPageNum++;
+                await this.renderPdfPage(this.pdfPageNum);
+            }
+        },
+
+        // 适应宽度
+        fitToWidth() {
+            this.scaleMode = 'width';
+            this.renderPdfPage(this.pdfPageNum);
+        },
+
+        // 处理缩放比例变化
+        handleScaleChange() {
+            this.scaleMode = 'scale';
+            this.renderPdfPage(this.pdfPageNum);
+        },
+
+        // 重试加载PDF
+        retryLoadPdf() {
+            if (this.currentFile.url) {
+                this.loadPdf(this.currentFile.url);
+            }
+        },
+
+        // 获取PDF错误信息
+        getPdfErrorMessage(error) {
+            if (error.name === 'PasswordException') {
+                return 'PDF文件受密码保护';
+            } else if (error.name === 'InvalidPDFException') {
+                return '无效的PDF文件';
+            } else if (error.name === 'MissingPDFException') {
+                return 'PDF文件不存在';
+            } else if (error.name === 'UnexpectedResponseException') {
+                return '服务器响应异常';
+            } else if (error.message.includes('NetworkError')) {
+                return '网络错误,请检查文件URL';
+            } else {
+                return '加载失败: ' + error.message;
+            }
+        },
+
+        // Excel相关方法
+        async loadExcel(url) {
+            try {
+                const response = await fetch(url);
+                const arrayBuffer = await response.arrayBuffer();
+                this.workbook = XLSX.read(arrayBuffer, { 
+                    type: 'array',
+                    cellDates: true, // 将日期单元格转换为JS日期对象
+                    cellText: false,
+                    cellNF: false
+                 });
+                this.sheets = this.workbook.SheetNames.map(name => ({ name }));
+                this.pickedSheet = this.sheets[0].name;
+                this.changeSheet(this.pickedSheet);
+            } catch (error) {
+                console.error('Excel加载失败:', error);
+                throw error;
+            }
+        },
+
+        changeSheet(name) {
+            this.pickedSheet = name;
+            const ws = this.workbook.Sheets[name];
+            if (!ws) return;
+
+            const { data, merges } = this.parseSheet(ws);
+            if (data.length === 0) {
+                this.tableData = [];
+            } else {
+                this.tableData = data;
+                this.originalMerges = merges;
+                this.mergeMap = this.buildMergeMap(merges);
+                this.$nextTick(() => {
+                    this.initTableLayout();
+                });
+            }
+        },
+
+        initTableLayout() {
+            // 确保表格有足够的宽度来触发横向滚动
+            const table = this.$refs.excelTable;
+            if (table) {
+                // 强制设置表格最小宽度,确保横向滚动生效
+                const minTableWidth = Math.max(table.scrollWidth, 1200); // 至少1200px或实际内容宽度
+                table.style.minWidth = minTableWidth + 'px';
+            }
+        },
+
+        parseSheet(ws) {
+            const range = XLSX.utils.decode_range(ws['!ref'] || 'A1');
+            const data = [];
+
+            for (let r = range.s.r; r <= range.e.r; r++) {
+                const row = [];
+                for (let c = range.s.c; c <= range.e.c; c++) {
+                    const cellAddress = XLSX.utils.encode_cell({ r, c });
+                    const cell = ws[cellAddress];
+
+                    let cellValue = '';
+                    if (cell) {
+                        // 处理日期
+                        if (cell instanceof Date) {
+                            cellValue = this.formatDate(cell);
+                        } else if (cell.t === 'd') {
+                            cellValue = this.formatDate(cell.v);
+                        } else {
+                            cellValue = cell.v;
+                        }
+                    }
+                    row.push(cellValue);
+                }
+                data.push(row);
+            }
+
+            const merges = ws['!merges'] || [];
+            return { data, merges };
+        },
+
+        // // 增强的日期格式化
+        // formatDate(date) {
+        //     if (!date) return '';
+
+        //     let dateObj;
+        //     if (date instanceof Date) {
+        //         dateObj = date;
+        //     } else if (typeof date === 'string') {
+        //         dateObj = new Date(date);
+        //     } else if (typeof date === 'number') {
+        //         dateObj = new Date((date - 25569) * 86400 * 1000); // Excel日期转换
+        //     } else {
+        //         return String(date);
+        //     }
+
+        //     if (isNaN(dateObj.getTime())) {
+        //         return String(date); // 无效日期返回原值
+        //     }
+
+        //     const year = dateObj.getFullYear();
+        //     const month = String(dateObj.getMonth() + 1).padStart(2, '0');
+        //     const day = String(dateObj.getDate()).padStart(2, '0');
+        //     const hours = String(dateObj.getHours()).padStart(2, '0');
+        //     const minutes = String(dateObj.getMinutes()).padStart(2, '0');
+        //     const seconds = String(dateObj.getSeconds()).padStart(2, '0');
+
+        //     // 判断是否只包含日期部分
+        //     const hasTime = hours !== '00' || minutes !== '00' || seconds !== '00';
+
+        //     if (hasTime) {
+        //         return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+        //     } else {
+        //         return `${year}-${month}-${day}`;
+        //     }
+        // },
+
+
+        // 修复日期格式化方法
+        formatDate(date) {
+            if (!date) return '';
+
+            let dateObj;
+
+            // 处理不同类型的日期输入
+            if (date instanceof Date) {
+                dateObj = date;
+            } else if (typeof date === 'string') {
+                // 尝试解析字符串日期
+                dateObj = new Date(date);
+                // 如果解析失败,尝试其他格式
+                if (isNaN(dateObj.getTime())) {
+                    // 尝试处理Excel日期字符串格式
+                    if (date.includes('/') || date.includes('-')) {
+                        const parts = date.split(/[/-]/);
+                        if (parts.length === 3) {
+                            dateObj = new Date(parts[0], parts[1] - 1, parts[2]);
+                        }
+                    }
+                }
+            } else if (typeof date === 'number') {
+                // Excel日期数字,使用专门的转换
+                return this.excelDateToJSDate(date);
+            } else {
+                return String(date);
+            }
+
+            // 检查日期是否有效
+            if (!dateObj || isNaN(dateObj.getTime())) {
+                console.warn('无效的日期:', date);
+                return String(date);
+            }
+
+            // 使用本地时间获取日期组件
+            const year = dateObj.getFullYear();
+            const month = String(dateObj.getMonth() + 1).padStart(2, '0');
+            const day = String(dateObj.getDate()).padStart(2, '0');
+            const hours = String(dateObj.getHours()).padStart(2, '0');
+            const minutes = String(dateObj.getMinutes()).padStart(2, '0');
+            const seconds = String(dateObj.getSeconds()).padStart(2, '0');
+
+            // 判断是否只包含日期部分
+            const hasTime = hours !== '00' || minutes !== '00' || seconds !== '00';
+
+            if (hasTime) {
+                return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+            } else {
+                return `${year}-${month}-${day}`;
+            }
+        },
+
+        // 增强的Excel日期转换
+        excelDateToJSDate(serial) {
+            try {
+                // Excel的日期从1900-01-01开始
+                const excelEpoch = new Date(1899, 11, 30); // 1899-12-30
+                const jsDate = new Date(excelEpoch.getTime() + (serial - 1) * 24 * 60 * 60 * 1000);
+
+                // 处理1900闰年错误(Excel认为1900是闰年)
+                if (serial >= 60) {
+                    // 从1900-03-01开始,需要减去一天
+                    jsDate.setTime(jsDate.getTime() - 24 * 60 * 60 * 1000);
+                }
+
+                return jsDate;
+            } catch (error) {
+                console.warn('Excel日期转换失败:', error, '原始值:', serial);
+                return new Date(serial); // 失败时尝试直接转换
+            }
+        },
+
+        buildMergeMap(merges) {
+            const map = {};
+            merges.forEach(merge => {
+                const { s, e } = merge;
+
+                map[`${s.r}-${s.c}`] = {
+                    rowspan: e.r - s.r + 1,
+                    colspan: e.c - s.c + 1,
+                    isMaster: true
+                };
+
+                for (let r = s.r; r <= e.r; r++) {
+                    for (let c = s.c; c <= e.c; c++) {
+                        if (r !== s.r || c !== s.c) {
+                            map[`${r}-${c}`] = {
+                                hidden: true
+                            };
+                        }
+                    }
+                }
+            });
+            return map;
+        },
+
+        getRowspan(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`];
+            return cellInfo && cellInfo.rowspan ? cellInfo.rowspan : 1;
+        },
+
+        getColspan(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`];
+            return cellInfo && cellInfo.colspan ? cellInfo.colspan : 1;
+        },
+
+        shouldShowCell(r, c) {
+            const cellInfo = this.mergeMap[`${r}-${c}`];
+            return !cellInfo || !cellInfo.hidden;
+        },
+
+        // 加载Word
+        async loadWord(url) {
+            try {
+                const response = await fetch(url);
+                const arrayBuffer = await response.arrayBuffer();
+
+                // 这里需要引入mammoth库来处理Word文档
+                if (typeof mammoth !== 'undefined') {
+                    const result = await mammoth.convertToHtml({ arrayBuffer });
+                    this.wordContent = result.value;
+                } else {
+                    // 如果没有mammoth库,显示提示信息
+                    this.wordContent = `
+            <div style="padding: 20px; text-align: center;">
+              <h3>Word文档预览</h3>
+              <p>如需预览Word文档,请引入mammoth.js库</p>
+              <p>当前文件: ${this.currentFile.name}</p>
+            </div>
+          `;
+                }
+
+                if (!this.wordContent) {
+                    throw new Error('Word文档转换失败');
+                }
+            } catch (error) {
+                console.error('Word加载失败:', error);
+                throw error;
+            }
+        },
+
+        // 图片缩放
+        zoomIn() {
+            if (this.imageScale < 3) {
+                this.imageScale += 0.1;
+            }
+        },
+
+        zoomOut() {
+            if (this.imageScale > 0.5) {
+                this.imageScale -= 0.1;
+            }
+        },
+
+        resetZoom() {
+            this.imageScale = 1;
+        },
+
+        // 下载文件
+        downloadFile() {
+            if (this.currentFile.url) {
+                const a = document.createElement('a');
+                a.href = this.currentFile.url;
+                a.download = this.currentFile.name;
+                document.body.appendChild(a);
+                a.click();
+                document.body.removeChild(a);
+            }
+        }
+    }
+};
+</script>
+
+<style scoped>
+.file-preview-container {
+    width: 100%;
+    height: 100%;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+}
+
+.preview-area {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    background-color: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+    overflow: hidden;
+}
+
+.preview-header {
+    padding: 15px 20px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #f8f9fa;
+}
+
+.preview-header h3 {
+    font-size: 18px;
+    color: #333;
+    margin: 0;
+}
+
+.file-actions {
+    display: flex;
+    gap: 10px;
+}
+
+.action-btn {
+    padding: 8px 15px;
+    background: #4b6cb7;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    gap: 5px;
+    transition: all 0.3s;
+    font-size: 14px;
+}
+
+.action-btn:hover {
+    background: #3a5795;
+}
+
+.preview-container {
+    flex: 1;
+    overflow: auto;
+    background-color: #f8f9fa;
+}
+
+.preview-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.preview-placeholder {
+    text-align: center;
+    color: #6c757d;
+    padding: 40px;
+}
+
+.placeholder-icon {
+    font-size: 64px;
+    margin-bottom: 15px;
+}
+
+.preview-placeholder h3 {
+    margin-bottom: 10px;
+    font-size: 20px;
+}
+
+.loading {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    height: 200px;
+    color: #6c757d;
+}
+
+.spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid rgba(0, 0, 0, 0.1);
+    border-left-color: #4b6cb7;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+    margin-bottom: 15px;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+.pdf-preview {
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.pdf-controls {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 15px;
+    gap: 10px;
+    flex-wrap: wrap;
+    padding: 10px;
+    background-color: white;
+    border-bottom: 1px solid #eaeaea;
+    width: 100%;
+}
+
+.pdf-controls button {
+    padding: 8px 15px;
+    background-color: #4b6cb7;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.3s;
+    display: flex;
+    align-items: center;
+    gap: 5px;
+    font-size: 14px;
+}
+
+.pdf-controls button:disabled {
+    background-color: #adb5bd;
+    cursor: not-allowed;
+}
+
+.pdf-controls button:hover:not(:disabled) {
+    background-color: #3a5795;
+}
+
+.page-info {
+    margin: 0 10px;
+    font-weight: 500;
+    font-size: 14px;
+}
+
+.scale-select {
+    padding: 8px;
+    border: 1px solid #ced4da;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+.pdf-viewer {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    overflow: auto;
+    background: white;
+    padding: 20px;
+    min-height: 500px;
+}
+
+.image-preview-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 20px;
+}
+
+.image-preview {
+    max-width: 100%;
+    max-height: 80%;
+    object-fit: contain;
+    transition: transform 0.3s;
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.image-actions {
+    margin-top: 15px;
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+
+.image-actions button {
+    padding: 8px 15px;
+    background-color: #f8f9fa;
+    border: 1px solid #dee2e6;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.3s;
+    font-size: 14px;
+}
+
+.image-actions button:hover {
+    background-color: #e9ecef;
+}
+
+.image-actions button:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+
+.zoom-level {
+    margin-left: 10px;
+    font-weight: 500;
+}
+
+.word-preview {
+    width: 100%;
+    height: 100%;
+    padding: 20px;
+}
+
+.word-content {
+    width: 100%;
+    height: 100%;
+    background-color: white;
+    padding: 30px;
+    border-radius: 4px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+    line-height: 1.6;
+    overflow: auto;
+}
+
+.word-content h1,
+.word-content h2,
+.word-content h3 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+}
+
+.word-content p {
+    margin-bottom: 1em;
+}
+
+.word-content table {
+    border-collapse: collapse;
+    width: 100%;
+    margin: 1em 0;
+}
+
+.word-content table,
+.word-content th,
+.word-content td {
+    border: 1px solid #ddd;
+    padding: 8px;
+}
+
+.word-content th {
+    background-color: #f2f2f2;
+}
+
+.no-content {
+    text-align: center;
+    padding: 40px;
+    color: #6c757d;
+}
+
+.unsupported-file {
+    text-align: center;
+    padding: 40px;
+}
+
+.unsupported-icon {
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.error-message {
+    text-align: center;
+    padding: 20px;
+    color: #dc3545;
+    background: #f8d7da;
+    border-radius: 8px;
+    margin: 20px;
+}
+
+/* Excel预览样式 */
+.excel-preview {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+.sheet-tabs {
+    margin-bottom: 15px;
+    border-bottom: 1px solid #eaeaea;
+    background-color: white;
+}
+
+.tabs-container {
+    display: flex;
+    overflow-x: auto;
+    gap: 2px;
+    padding-bottom: 2px;
+}
+
+.tab-item {
+    padding: 8px 16px;
+    background: #f5f5f5;
+    border: 1px solid #ddd;
+    border-bottom: none;
+    border-radius: 4px 4px 0 0;
+    cursor: pointer;
+    white-space: nowrap;
+    transition: all 0.3s ease;
+    flex-shrink: 0;
+}
+
+.tab-item:hover {
+    background: #e9e9e9;
+}
+
+.tab-item.active {
+    background: #4b6cb7;
+    color: white;
+    border-color: #4b6cb7;
+}
+
+.tab-name {
+    font-size: 14px;
+    font-weight: 500;
+}
+
+.excel-info {
+    display: flex;
+    gap: 20px;
+    padding: 12px 20px;
+    background: #f8f9fa;
+    border-radius: 6px;
+    margin: 0 20px 15px 20px;
+    flex-wrap: wrap;
+}
+
+.info-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.info-item .label {
+    font-weight: 600;
+    color: #495057;
+    font-size: 14px;
+}
+
+.info-item .value {
+    color: #4b6cb7;
+    font-weight: 500;
+    font-size: 14px;
+}
+
+.table-container {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    border: 1px solid #e0e0e0;
+    border-radius: 6px;
+    background: white;
+    overflow: hidden;
+    margin: 0 20px 20px 20px;
+}
+
+.horizontal-scrollbar {
+    height: 12px;
+    background: #f5f5f5;
+    border-bottom: 1px solid #e0e0e0;
+    overflow-x: auto;
+    overflow-y: hidden;
+}
+
+.scrollbar-track {
+    height: 1px;
+}
+
+.table-wrapper {
+    flex: 1;
+    overflow: auto;
+    position: relative;
+}
+
+.excel-table {
+    border-collapse: collapse;
+    table-layout: auto;
+    min-width: 100%;
+    font-size: 13px;
+}
+
+.excel-table tr {
+    height: 35px;
+}
+
+.excel-table td {
+    border: 1px solid #e0e0e0;
+    padding: 4px 8px;
+    vertical-align: middle;
+    position: relative;
+    min-width: 100px;
+    max-width: 300px;
+    word-wrap: break-word;
+    overflow: hidden;
+}
+
+.header-row {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+}
+
+.header-cell {
+    background: #f8f9fa !important;
+    font-weight: 600;
+    color: #2c3e50;
+    border-bottom: 2px solid #4b6cb7 !important;
+}
+
+.data-cell {
+    background: white;
+}
+
+.data-cell:nth-child(even) {
+    background: #fafafa;
+}
+
+.data-cell:hover {
+    background: #f0f7ff !important;
+}
+
+.cell-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: #909399;
+}
+
+.empty-icon {
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.empty-state h3 {
+    margin-bottom: 10px;
+    font-size: 18px;
+}
+
+/* 滚动条样式 */
+.table-wrapper::-webkit-scrollbar {
+    width: 12px;
+    height: 12px;
+}
+
+.table-wrapper::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 6px;
+}
+
+.table-wrapper::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 6px;
+    border: 2px solid #f1f1f1;
+}
+
+.table-wrapper::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+.table-wrapper::-webkit-scrollbar-corner {
+    background: #f1f1f1;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar {
+    height: 8px;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 4px;
+}
+
+.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .excel-info {
+        flex-direction: column;
+        gap: 8px;
+        margin: 0 10px 10px 10px;
+    }
+
+    .info-item {
+        justify-content: space-between;
+    }
+
+    .tab-item {
+        padding: 6px 12px;
+        font-size: 13px;
+    }
+
+    .excel-table {
+        font-size: 12px;
+    }
+
+    .excel-table td {
+        padding: 3px 6px;
+        min-width: 80px;
+    }
+
+    .table-container {
+        margin: 0 10px 10px 10px;
+    }
+
+    .sheet-tabs {
+        padding: 0 10px;
+    }
+}
+</style>

+ 0 - 1
src/okr/views/targetBusiness/cycleList.vue

@@ -209,7 +209,6 @@ export default {
             }
             this.loading = true
             this.$axiosUser('get', url, params).then(res => {
-                console.log("获取周期列表");
                 this.loading = false;
                 this.cycleList = res.data.data.list || []
                 this.total = res.data.data.total;

+ 3 - 9
src/okr/views/targetBusiness/dataStatistics.vue

@@ -283,7 +283,6 @@ export default {
                 this.treeCheckBox = false
             }
             this.parentGoalsData = records
-            console.log(this.parentGoalsData);
         },
 
         customChangeSetting() {
@@ -312,11 +311,9 @@ export default {
         },
 
         handleParentExpanded(parent) {
-            console.log('父节点展开:', parent.name)
             // 可以在这里处理展开逻辑,比如更新其他组件状态
         },
         handleParentCollapsed() {
-            console.log('父节点收起')
             // 可以在这里处理收起逻辑
         },
 
@@ -425,7 +422,6 @@ export default {
             const result = this.findGoalLevelAndSiblings(this.tableData, row.goalId);
 
             if (result) {
-                console.log('找到目标:', result);
                 this.parentGoalsData = result.siblings; // 同层级的目标列表
             } else {
                 // 如果没找到,使用默认逻辑
@@ -433,7 +429,6 @@ export default {
                     this.parentGoalsData = this.tableData;
                 } 
             }
-            console.log('最终传递的目标列表:', this.parentGoalsData);
             this.dataCenterDialogVisible = true;
         },
 
@@ -522,7 +517,7 @@ export default {
                     let targetValues = this.rawData.quarterModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                     let resultValues = this.rawData.quarterModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                     targetValues.forEach((value, index) => {
-                        obj.values.push({ value: resultValues[index] - targetValues[index] })
+                        obj.values.push({ value: (resultValues[index].value - targetValues[index].value).toFixed(2) })
                     })
                     this.rawData.quarterModel.data.push(obj)
                     obj = {
@@ -536,7 +531,7 @@ export default {
                     let targetValues = this.rawData.monthModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                     let resultValues = this.rawData.monthModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                     targetValues.forEach((value, index) => {
-                        obj.values.push({ value: resultValues[index] - targetValues[index] })
+                        obj.values.push({ value: (resultValues[index].value - targetValues[index].value).toFixed(2) })
                     })
                     this.rawData.monthModel.data.push(obj)
                     obj = {
@@ -550,7 +545,7 @@ export default {
                     let targetValues = this.rawData.dayModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
                     let resultValues = this.rawData.dayModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
                     targetValues.forEach((value, index) => {
-                        obj.values.push({ value: resultValues[index] - targetValues[index] })
+                        obj.values.push({ value: (resultValues[index].value - targetValues[index].value).toFixed(2) })
                     })
                     this.rawData.dayModel.data.push(obj)
                     obj = {
@@ -559,7 +554,6 @@ export default {
                         values: []
                     }
                 }
-                console.log(this.rawData);
                 this.cycleTableDialogVisible = true
             })
         }

+ 11 - 5
src/okr/views/targetBusiness/deptTargetList.vue

@@ -144,7 +144,7 @@
 
         <!-- 上传附件 -->
         <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="currentRow.goalId"
-            :files="files" @confirm="confirmAppendFile" />
+            :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
 
         <el-dialog class="data-center-box" title="目标详情" :visible.sync="dataCenterDialogVisible" @close="handleClose()"
             :close-on-click-modal="false" :close-on-press-escape="true" center fullscreen :show-close="false"
@@ -294,11 +294,18 @@ export default {
             let url = `/okr/og/scope/${this.user_info.site_id}`
             let res = await this.$axiosUser('get', url)
             let list = res.data.data.list || []
+
             if (list && list.length > 0) {
                 this.cycleList = list.filter(item => item.status == 1)
-                this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
-                if (!this.scopeId) this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
-            }
+                this.cycleList.forEach(item => item.scopeId = item.scopeId.toString())
+                if (this.scopeId) {
+                    let cycleType = this.cycleList.find(cycle => cycle.scopeId == this.scopeId).cycleType || 4
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == cycleType)
+                } else {
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
+                    this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
+                }
+            } 
         },
 
         // 获取公司部门列表
@@ -345,7 +352,6 @@ export default {
                     hasChild: item.subCount > 0 ? true : false, // 告诉 vxe 是否显示“+”
                     loading: false // 初始化子节点的加载状态
                 }))
-                console.log(this.tableData)
                 this.total = res.data.data.total;
                 this.loading = false;
             })

+ 17 - 11
src/okr/views/targetBusiness/personalTargetList.vue

@@ -26,7 +26,7 @@
             <el-select v-if="cycleList.length !== 0" class="select" size="small" v-model="scopeId" placeholder="周期"
                 style="width: 200px; " @change="changeCycleType">
                 <el-option v-for="item in filterCycleList" :key="item.scopeId" :label="item.remark"
-                    :value="item.scopeId + ''"> 
+                    :value="item.scopeId + ''">
                 </el-option>
             </el-select>
 
@@ -387,7 +387,7 @@
 
         <!-- 上传附件 -->
         <AppendFileUpload v-if="fileUploadVisible" v-model="fileUploadVisible" :goalId="currentRow.goalId"
-            :files="files" @confirm="confirmAppendFile" />
+            :files="files" :ownerId="user_info.id" @confirm="confirmAppendFile" />
 
         <!-- 负责人确认状态 进入 制定阶段 调整 红线值,挑战值,超越值,预计达成值,完成值 -->
         <EditBaseTableData v-if="dialogVisible" v-model="dialogVisible" :editTableData="editTableData"
@@ -707,7 +707,6 @@ export default {
             }
             
             this.$http.post(url, data).then(res => {
-                console.log(res);
                 if (res.code == 1) {
                     this.tableData.splice(this.currentRowIndex, 1, res.data)
                     this.$message.success('操作成功')
@@ -756,19 +755,26 @@ export default {
             history.pushState(null, null, location.href);
         },
 
-
         // 获取目标周期列表
         async getCycleList() {
             let url = `/okr/og/scope/${this.user_info.site_id}`
             let res = await this.$axiosUser('get', url)
             let list = res.data.data.list || []
+
             if (list && list.length > 0) {
                 this.cycleList = list.filter(item => item.status == 1)
-                this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
-                if (!this.scopeId) this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
-            }
+                this.cycleList.forEach(item => item.scopeId = item.scopeId.toString())
+                if (this.scopeId) {
+                    let cycleType = this.cycleList.find(cycle => cycle.scopeId == this.scopeId).cycleType || 4
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == cycleType)
+                } else {
+                    this.filterCycleList = this.cycleList.filter(item => item.cycleType == 4)
+                    this.scopeId = this.filterCycleList.length > 0 ? this.filterCycleList[0].scopeId.toString() : ''
+                }
+            } 
         },
 
+
         // 获取公司部门列表
         async getDeptList() {
             let res = await this.$axiosUser('get', '/api/pro/department/tree', '', 'v2')
@@ -1195,7 +1201,6 @@ export default {
         },
 
         employee_confirm(data) {
-            console.log(data)
             this.employee_selected = { dept: [], employee: [] };
             this.employeeVisibleName = '';
             let employee_ids = [];
@@ -1213,10 +1218,11 @@ export default {
 
         confirmAppendFile(row) {
             if (row) {
-                this.currentRow = row
-                this.tableData.splice(this.currentRowIndex, 1, row)
-                this.currentRow = null
                 this.fileUploadVisible = false;
+                this.getData();
+                // this.currentRow = row
+                // this.tableData.splice(this.currentRowIndex, 1, row)
+                // this.currentRow = null
             }
 
         },

+ 17 - 6
src/okr/views/targetBusiness/previewFile.vue

@@ -7,7 +7,7 @@
             </header>
 
             <div class="main-content">
-                <div class="sidebar">
+                <!-- <div class="sidebar">
                     <div class="upload-section">
                         <input type="file" id="fileInput" ref="fileInput" @change="handleFileUpload"
                             accept=".pdf,.xlsx,.xls,.jpg,.jpeg,.png,.docx" style="display: none">
@@ -28,7 +28,7 @@
                             <button class="remove-btn" @click.stop="removeFile(file.id)">×</button>
                         </li>
                     </ul>
-                </div>
+                </div> -->
 
                 <div class="preview-area">
                     <div class="preview-header">
@@ -209,8 +209,8 @@
 
 <script>
 import * as XLSX from 'xlsx'
-import ExcelPreview from "@/components/ExcelPreview";
-import PdfPreview from "@/components/PdfPreview";
+// import ExcelPreview from "@/components/ExcelPreview";
+// import PdfPreview from "@/components/PdfPreview";
 // import pdfFileUrl from "@/assets/123.pdf"
 // import { previewIndicatorFile } from "@/api/dataFilling/indicatorDisposal";
 // 设置PDF.js worker路径 - 使用稳定版本的Worker
@@ -218,6 +218,12 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs
 
 export default {
     name: "IndicationPreview",
+    prop: {
+        file: {
+            type: Object,
+            default: () => ({})
+        }
+    },
     data() {
         return {
             currentFile: {},
@@ -251,6 +257,12 @@ export default {
         };
     },
 
+    watch: {
+        file(oVal, nVal) {
+            this.selectFile(file)
+        }
+    },
+
     methods: {
         // 获取文件图标
         getFileIcon(type) {
@@ -314,8 +326,7 @@ export default {
 
         // 选择文件
         async selectFile(file) {
-
-            console.log(file)
+            
             this.currentFile = file;
             this.loading = true;
             this.loadProgress = 0;

+ 1759 - 0
src/okr/views/targetBusiness/targetDetail copy.vue

@@ -0,0 +1,1759 @@
+<template>
+    <div class="all target-detail-box" v-loading="loading">
+
+        <div class="top" :class="currentTab == '概览' ? 'flex-1' : 'h-100'">
+
+            <div style="padding: 10px 20px 0 20px; box-sizing: border-box; justify-content: space-between;"
+                class="flex-box-ce">
+                <header class="PageHead flex-box-ce">
+                    <span class="return" @click="goPage()">返回</span>
+                    <span style="font-size: 16px; font-weight: 600;">目标详情</span>
+                    <span class="fontColorC" style="margin-left: 10px;">对齐目标: {{ targetDetail.parentName || '--'
+                        }}</span>
+                </header>
+                <div class="flex-box-ce">
+                    <!-- <el-button type="primary" size="small" plain @click="getDataCenter">数据大屏</el-button> -->
+                    <el-button type="primary" size="small" plain @click="getDetails">刷新数据</el-button>
+                    <el-tooltip effect="dark" content="教程指引" placement="top">
+                        <div class="icon flex-center-center" @click="initStepData()">
+                            <i class="el-icon-document" id="startTour"></i>
+                        </div>
+                    </el-tooltip>
+                </div>
+            </div>
+
+            <el-alert v-if="targetDetail.status == 0" class="bounce animated" type="warning" title="" :closable="false"
+                show-icon style="width: 99%; margin: 0 auto;">
+                <template slot="title">
+                    <div class="alert-title" style="width: 100%;">
+                        <div class="flex-1">{{ alertTilte }}</div>
+                        <el-link type="primary" size="mini" @click="openTableData()">
+                            进入目标制定阶段
+                        </el-link>
+                    </div>
+                </template>
+            </el-alert>
+
+
+            <div class="target-info">
+                <div class="flex-box-ce">
+                    <div class="type-img-box">
+                        <svg-icon v-if="targetDetail.type == 1" icon-class="#icon-gongsi" class="svgIcon"></svg-icon>
+                        <svg-icon v-if="targetDetail.type == 2" icon-class="#icon-bumenguanli"
+                            class="svgIcon"></svg-icon>
+                        <svg-icon v-if="targetDetail.type == 3" icon-class="#icon-yonghu" class="svgIcon"></svg-icon>
+                    </div>
+                    <div>
+
+                        <div class="name flex-box-ce">
+                            {{ targetDetail.name }}
+
+                            <!-- 目标状态 -->
+                            <TargetStatus :targetDetail="targetDetail" @showTableData="openTableData()"
+                                @confirm="getDetails" />
+
+                        </div>
+
+
+                        <div class="flex-box-ce">
+                            <div class="tag" v-if="targetDetail.type == 1">
+                                <svg-icon icon-class="#icon-gongsi" class="svgIcon"
+                                    style="margin: 0 5px 0 0;"></svg-icon>公司目标
+                            </div>
+                            <div class="tag" v-if="targetDetail.type == 2">
+                                <svg-icon icon-class="#icon-bumenguanli" class="svgIcon"
+                                    style="margin: 0 5px 0 0;"></svg-icon>部门目标
+                            </div>
+                            <div class="tag" v-if="targetDetail.type == 3">
+                                <svg-icon icon-class="#icon-yonghu" class="svgIcon"
+                                    style="margin: 0 5px 0 0;"></svg-icon>个人目标
+                            </div>
+                            <div class="tag">
+                                <svg-icon icon-class="#icon-yonghu" class="svgIcon"
+                                    style="margin: 0 5px 0 0;"></svg-icon>
+                                负责人: {{ ownerName }}
+                            </div>
+
+
+                            <div class="tag" v-if="targetDetail.scope.cycleType == 1"> <i
+                                    class="el-icon-alarm-clock"></i> 年度 {{ targetDetail.scope.name }}</div>
+                            <div class="tag" v-if="targetDetail.scope.cycleType == 2"> <i
+                                    class="el-icon-alarm-clock"></i> 半年度 {{ targetDetail.scope.name }}</div>
+                            <div class="tag" v-if="targetDetail.scope.cycleType == 3"> <i
+                                    class="el-icon-alarm-clock"></i> 季度 {{ targetDetail.scope.name }}</div>
+                            <div class="tag" v-if="targetDetail.scope.cycleType == 4"> <i
+                                    class="el-icon-alarm-clock"></i> 月度 {{ targetDetail.scope.name }}</div>
+                            <el-link type="primary" @click="openTargetSearch()">
+                                <i class="el-icon-link"></i>
+                                {{ targetDetail.krName ? targetDetail.objectName + ' - ' + targetDetail.krName :
+                                '关联KR'}}
+                            </el-link>
+                        </div>
+
+                        <div class="flex-box-ce" style="margin: 5px 0;">
+                            <div class="tag">
+                                <i class="el-icon-document"></i>
+                                目标说明: {{ targetDetail.content || '--' }}
+                            </div>
+
+                        </div>
+
+                        <div class="flex-box-ce">
+                            <div class="tag">
+                                <i class="el-icon-document"></i>
+                                目标标准: {{ targetDetail.standard || '--' }}
+                            </div>
+                        </div>
+
+                        <!-- 自动量化配置,预计达成值,完成值是否由子目标决定 -->
+                        <div class="flex-box-ce switch-box">
+                            <EditAutoSubSwitch v-if="targetDetail.goalId" :targetDetail="targetDetail"
+                                @confirm="getDetails" />
+                        </div>
+
+
+                    </div>
+
+                </div>
+
+                <div class="flex-box-ce">
+                    <div class="chart1">
+                        <GaugeChart :percent="percent" />
+                    </div>
+                </div>
+            </div>
+
+            <div class="tab-list">
+                <div class="tab-item" @click="changeTab(index)"
+                    :class="[ item.class, current == index ? 'active' : '' ]" v-for="(item, index) in tabs"
+                    :key="item.id">
+                    {{ item.name }}
+                    <div v-show="current == index" class="line"></div>
+                </div>
+            </div>
+            <!-- 基础值列表 -->
+            <CardList v-if="currentTab == '概览'" :targetDetail="targetDetail" />
+
+            <div class="flex-1">
+                <!-- 拆解目标 -->
+                <DismantlingTarget ref="dismantlingTargetRef" v-if="currentTab == '拆解目标'"
+                    :targetDetail="targetDetail" />
+
+                <!-- 任务列表 -->
+                <TaskList v-if="currentTab == '任务'" :targetDetail="targetDetail" />
+
+                <!-- 任务列表 -->
+                <MessageList v-if="currentTab == '动态'" :targetDetail="targetDetail" />
+            </div>
+        </div>
+
+        <div class="bottom" v-if="current == 0" :style="{ height: '52%' } ">
+            <template v-if="!autoBySub">
+                <div class="title-box">
+                    <div class="title flex-box-ce">
+                        目标时间计划
+                        <el-button v-if="isCanSvgTarget" type="primary" size="mini" plain round @click="svgTargetNums()"
+                            style="margin-left: 10px;">一键平均分配</el-button>
+
+                        <el-button type="primary" size="mini" plain round @click="exportEvent()"
+                            style="margin-left: 10px;">导出</el-button>
+                    </div>
+                    <div v-if="targetDetail.planModels && targetDetail.planModels.data">
+                        <el-link @click="deletePlan()" type="danger"
+                            v-if="targetDetail.status == 0 || targetDetail.status == 1">删除计划</el-link>
+                    </div>
+                </div>
+
+
+                <div class="table-box">
+                    <template v-if="targetDetail.planModels && targetDetail.planModels.data">
+                        <vxe-table ref="xTable" :data="targetDetail.planModels.data"
+                            :column-config="{ resizable: true }" :row-config="{ isHover: true, isCurrent: true }"
+                            max-height="auto" border auto-resize :edit-config="editConfig"
+                            @edit-actived="handleEditActived" @edit-closed="handleEditClosed">
+                            <vxe-column field="name" :title="'指标(' + targetDetail.unit + ')'" min-width="200"
+                                fixed="left">
+                                <template #default="{ row }">
+                                    <div class="flex-box-ce">
+                                        <div v-if="row.code === 'RISK_VALUE'" class="line"
+                                            style=" background: #FFEEEB; ">
+                                        </div>
+                                        <div v-if="row.code === 'TARGET_VALUE'" class="line"
+                                            style=" background: #E9F8F7; ">
+                                        </div>
+                                        <div v-if="row.code === 'CHALLENGE_VALUE'" class="line"
+                                            style=" background: #FCF9ED; ">
+                                        </div>
+                                        <div v-if="row.code === 'SURPASS_VALUE'" class="line"
+                                            style=" background: #FFF7EE; ">
+                                        </div>
+                                        <div v-if="row.code === 'PROJECTED_VALUE'" class="line"
+                                            style=" background: #F3F3FE; "></div>
+                                        <div v-if="row.code === 'RESULT_VALUE'" class="line"
+                                            style=" background: #c6f7de; ">
+                                        </div>
+                                        <div>{{ row.name }}</div>
+                                    </div>
+                                </template>
+                            </vxe-column>
+                            <template v-if="targetDetail.planModels && targetDetail.planModels.scopes.length > 0">
+
+                                <vxe-column v-for="(header, index) in targetDetail.planModels.scopes"
+                                    :key="header.scopeId" :title="header.scopeName" :field="header.scopeId"
+                                    min-width="100" align="left" :edit-render="{
+                                        name: 'input',
+                                        attrs: { type: 'number' },
+                                        style: { textAlign: 'left' }
+                                    }">
+                                    <template #default="{ row }">
+                                        <template v-for="value in row.values">
+                                            <span v-if="value.scopeId == header.scopeId"
+                                                :class="[row.allowEdit ? 'cursor-pointer' : '', row.code === 'DIFF_VALUE' ? value.value > 0 ? 'color-green' : 'color-red' : '']">{{
+                                                value.value
+                                                }}</span>
+                                        </template>
+                                    </template>
+                                </vxe-column>
+
+                            </template>
+                        </vxe-table>
+                    </template>
+                    <div v-else class="flex-box-ce" style="justify-content: center; width: 100%; height: 100%;">
+                        <!-- 只有周期为月度才能按日拆分计划 -->
+                        <el-button v-if="targetDetail.scope.cycleType == 4" type="primary" round
+                            @click="planByDate('day')">按日规划</el-button>
+                        <!-- 只有周期为年度,半年度,季度才能按月拆分计划 -->
+                        <el-button
+                            v-if="targetDetail.scope.cycleType == 1 || targetDetail.scope.cycleType == 2 || targetDetail.scope.cycleType == 3"
+                            type="primary" round @click="planByDate('month')">按月规划</el-button>
+                        <!-- 只有周期为年度,半年度才能按季拆分计划 -->
+                        <el-button v-if="targetDetail.scope.cycleType == 1 || targetDetail.scope.cycleType == 2"
+                            type="primary" round @click="planByDate('quarter')">按季规划</el-button>
+                    </div>
+                </div>
+            </template>
+
+            
+
+
+            <template v-else>
+                <div class=" title-box">
+                    <div class="title flex-box-ce">
+                        目标时间计划 {{ rawData }}
+                    </div>
+                    <div>
+                        <el-tooltip class="item" effect="dark" content="全屏" placement="top-start">
+                            <el-button @click="cycleTableDialogVisible = true" type="primary" icon="el-icon-full-screen"
+                                circle size="small"></el-button>
+                        </el-tooltip>
+                    </div>
+                </div>
+                <MultiCycleTable v-if="rawData" :rawData="rawData" :isCardListShow="false"
+                    :name="targetDetail.name || ''" :owner-name="ownerName" />
+            </template>
+
+        </div>
+
+
+        <TargetSearch :visible.sync="showTargetSearch" :selectedCheckList="checkList" @confirm="confirmSelectedOkr">
+        </TargetSearch>
+
+        <EditBaseTableData v-if="dialogVisible" v-model="dialogVisible" :targetDetail="targetDetail"
+            @confirm="getDetails" />
+
+        <el-dialog title="平均分配数据" center :visible.sync="openSvgTargetDialog" width="1200px"
+            :close-on-press-escape="false" append-to-body :close-on-click-modal="false">
+            <div>
+                <el-alert style="margin-bottom: 10px;" type="warning" title="将各指标目标数按规划周期整数均摊,余数并入末周期。"
+                    :closable="false" show-icon></el-alert>
+                <template>
+                    <vxe-table :data="svgTargetData.data" :column-config="{ resizable: true }"
+                        :row-config="{ isHover: true, isCurrent: true }" max-height="400px" border auto-resize
+                        :edit-config="{enable: false}">
+                        <vxe-column field="name" :title="'指标(' + targetDetail.unit + ')'" min-width="100" fixed="left">
+                        </vxe-column>
+                        <template v-for="(header, index) in svgTargetData.scopes">
+
+                            <vxe-column :key="header.scopeId" :title="header.scopeName" :field="header.scopeId"
+                                min-width="100" align="left">
+                                <template #default="{ row }">
+                                    <template v-for="value in row.values">
+
+                                        <span v-if="value.scopeId == header.scopeId"
+                                            :class="[row.allowEdit ? 'cursor-pointer' : '']">{{ value.value }}</span>
+                                    </template>
+                                </template>
+                            </vxe-column>
+                        </template>
+                    </vxe-table>
+                </template>
+            </div>
+
+            <div slot="footer">
+                <el-button type="primary" size="small" @click="saveSvgTargetNums()">保 存</el-button>
+                <el-button size="small" @click="resetData()">关 闭</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog class="multi-cycle-table" :title="targetDetail.name" :visible.sync="cycleTableDialogVisible"
+            @close="handleCycleTableClose()" :close-on-click-modal="false" :close-on-press-escape="true" center
+            fullscreen :show-close="false" @open="handleCycleTableOpen()">
+            <div style=" width: 100%; height: 100%; position: relative; ">
+                <el-button round style="position: absolute; top: -45px; left: 10px; z-index: 99;"
+                    @click="cycleTableDialogVisible = false">返回</el-button>
+                <!-- 引入多周期表格组件 -->
+                <MultiCycleTable v-if="rawData" :rawData="rawData" :name="targetDetail.name" :owner-name="ownerName" />
+            </div>
+        </el-dialog>
+
+        <el-dialog class="data-center-box" title="目标详情" :visible.sync="dataCenterDialogVisible" @close="handleClose()"
+            :close-on-click-modal="false" :close-on-press-escape="true" center fullscreen :show-close="false"
+            @open="onDialogOpen()" style="width: 100%; height: 100%;">
+            <div style="width: 100%; height: 100%; position: relative;">
+                <el-button round style="position: absolute; top: -65px; left: 0px; z-index: 99;"
+                    @click="handleClose">返回</el-button>
+                <DataCenter v-if="dataCenterDialogVisible" :outer-refresh="outerRefresh" :goal-id="goalId"
+                    :title="title" :parentGoalsData="[]" :scopeId="scopeId" :cycleType="cycleType" />
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+
+
+<script>
+import { mapGetters } from 'vuex';
+import GaugeChart from './components/TargetDetail/GaugeChart.vue'
+import PageHead from '@/components/PageHead'; //头部---返回
+import CardList from './components/CardList'; // 卡片列表
+import DismantlingTarget from './components/TargetDetail/DismantlingTarget'; // 拆解目标
+import MultiCycleTable from './components/MultiCycleTable.vue'; // 多周期表格
+import TargetStatus from "./components/TargetDetail/TargetStatus.vue"
+import TaskList from './components/TargetDetail/TaskList.vue';
+import MessageList from './components/TargetDetail/MessageList.vue';
+import EditAutoSubSwitch from "./components/TargetDetail/EditAutoSubSwitch.vue"
+import TargetSearch from "./components/TargetDetail/TargetSearchSingle.vue"
+import DataCenter from './components/DataCenter.vue'
+import EditBaseTableData from "./components/TargetDetail/EditBaseTableData.vue"
+import { splitTarget } from './utils/splitTarget.js'
+import { exportArrayToExcel } from './utils/exportTable.js'
+import cloneDeep from 'lodash.clonedeep';
+import introJs from 'intro.js'
+import 'intro.js/introjs.css'
+import targetDetailSteps from '@/okr/views/targetBusiness/utils/step/targetDetailSteps.js';
+import { targetDetail, planModels, rawData } from '@/okr/views/targetBusiness/utils/step/mock.js';
+
+export default {
+    components: {
+        GaugeChart,
+        CardList,
+        DismantlingTarget,
+        PageHead,
+        TargetStatus,
+        EditAutoSubSwitch,
+        TaskList,
+        MessageList,
+        TargetSearch,
+        DataCenter,
+        EditBaseTableData,
+        MultiCycleTable
+    },
+
+    data() {
+        return {
+            goalId: '',
+            routePush: null,
+            loading: false,
+            alertTilte: "当前目标正处于待确认状态,请确认好红线值,目标值,挑战值,超越值,预估达成值,完成值,开启实施目标的第一步吧!",
+            showTargetSearch: false,
+            checkList: '',
+            tabs: [
+                { id: 1, name: "概览", class: 'item-overview' },
+                { id: 2, name: "拆解目标", class: 'item-dismantling' },
+                { id: 3, name: "任务", class: 'item-task' },
+                { id: 4, name: "动态", class: 'item-dynamic' }
+            ],
+            employeeMap: this.$getEmployeeMap(),
+            dialogVisible: false,
+            editBaseData: [],
+            title: "",
+            targetDetail: {
+                content: "",
+                deptId: 0,
+                deptName: "",
+                endDate: "",
+                goalId: 1,
+                krId: 0,
+                krName: "",
+                name: "",
+                oid: 0,
+                oname: "",
+                ownerId: 0,
+                ownerName: "",
+                pid: 0,
+                planCount: 0,
+                planFinishCount: 0,
+                planModels: [],
+                pname: "",
+                publishId: "",
+                quantifyModels: [],
+                scope: {},
+                scopeId: 1,
+                standard: "",
+                startDate: "",
+                status: 0,
+                subCompleteCount: 0,
+                subConfirmCount: 0,
+                subCount: 4,
+                subReadyCount: 4,
+                subRunningCount: 0,
+                type: 1,
+                unit: ""
+            },
+            current: 0,
+            currentTab: '概览',
+            autoBySub: false, // 预计达成值, 完成值是否与子目标决定
+            editInitialMap: {},
+            openSvgTarget: true,
+            svgTargetData: [],
+            openSvgTargetDialog: false,
+            cycleTableDialogVisible: false,
+            title: "",
+            dataCenterDialogVisible: false,
+            outerRefresh: 0,
+            cycleType: '',
+            initialPlanModels: null,
+            rawData: {
+                name: "",
+                quarterModel: null,
+                monthModel: null,
+                dayModel: null,
+            }
+
+        }
+    },
+
+    
+    watch: {
+        autoBySub(oVal, nVal) {
+            if (!nVal) this.getStatisticsData();
+        }
+    },
+
+
+    computed: {
+        ...mapGetters(['user_info']),
+        
+        ownerName() {
+            return this.employeeMap[this.targetDetail.ownerId] && this.employeeMap[this.targetDetail.ownerId].name || '--'
+        },
+        // 创始人,总经理
+        isCreator() {
+            return this.$supremeAuthority('creator')
+        },
+        //是否目标管理员
+        isOkrManager() {
+            return this.user_info.is_okr_manager
+        },
+        // 创始人,OKR管理员,负责人可以拆解目标
+        isAllowDismantlingTarget() {
+            return this.isCreator || this.isOkrManager || this.user_info.id == this.targetDetail.ownerId
+        },
+
+        
+        editConfig() {
+            return ({
+                trigger: 'dblclick',
+                mode: 'cell',
+                beforeEditMethod: this.checkCell,     // ← 动态开关
+                autoFocus: true
+            })
+        },
+        
+        // 计算完成率
+        percent() {
+            let quantifyModels = this.targetDetail && this.targetDetail.quantifyModels || []
+            if (quantifyModels.length == 0) return 0
+            let target = quantifyModels.find(item => item.code == 'TARGET_VALUE').value // 目标值
+            let result = quantifyModels.find(item => item.code == 'RESULT_VALUE').autoBySub ? quantifyModels.find(item => item.code == 'RESULT_VALUE').subValue : quantifyModels.find(item => item.code == 'RESULT_VALUE').value // 完成值
+            if (target == 0) return 0
+            if (result == 0) return 0
+            else return result ? Number(result / target) * 100 : 0
+        },
+
+        // 是否可以一键平分目标isCanSvgTarget
+        isCanSvgTarget() {
+            return (this.targetDetail.status == 0 || this.targetDetail.status == 1) && this.targetDetail.planModels
+        },
+
+        // 是否有季度数据
+        isHaveQuarterModelData() {
+            return this.rawData && this.rawData.quarterModel && this.rawData.quarterModel.scopes && this.rawData.quarterModel.scopes.length > 0
+        },
+        // 是否有月度数据
+        isHaveMonthModelData() {
+            return this.rawData && this.rawData.monthModel && this.rawData.monthModel.scopes && this.rawData.monthModel.scopes.length > 0
+        },
+        // 是否有日度数据
+        isHaveDayModelData() {
+            return this.rawData && this.rawData.dayModel && this.rawData.dayModel.scopes && this.rawData.dayModel.scopes.length > 0
+        },
+    },
+
+    mounted() {
+        this.$nextTick(() => {
+            this.goalId = this.$route.params.goalId;
+            let scopeId = this.$route.query.scopeId, cycleType = this.$route.query.cycleType
+            this.cycleType = this.$route.query.cycleType
+            this.routePush = { path: this.$route.query.from, query: { scopeId, cycleType } }
+            this.getDetails();
+            
+            if (localStorage.getItem("isTargetLearned")) {
+                if (!localStorage.getItem('tourTargetDetail')) {
+                    this.initStepData();
+                    localStorage.setItem('tourTargetDetail', 'true');
+                }
+            }
+        });
+    },
+
+
+    methods: {
+
+        initStepData() {
+            this.current = 0
+            this.currentTab = '概览'
+            this.autoBySub = true
+            this.tabs = [
+                { id: 1, name: "概览", class: 'item-overview' },
+                { id: 2, name: "拆解目标", class: 'item-dismantling' },
+                { id: 3, name: "任务", class: 'item-task' },
+                { id: 4, name: "动态", class: 'item-dynamic' }
+            ];
+            this.targetDetail = targetDetail;
+            this.targetDetail.planModels = { data: null }
+            this.rawData = rawData
+            this.startGuide();
+        },
+
+        startGuide() {
+            let that = this
+            introJs().setOptions({
+                nextLabel: '下一个',  // 下一个按钮文字
+                prevLabel: '上一个',  // 上一个按钮文字
+                skipLabel: '跳过',    // 跳过按钮文字
+                doneLabel: '立即体验',// 完成按钮文字
+                tooltipClass: 'intro-tooltip', /* 引导说明文本框的样式 */
+                highlightClass: 'intro-highlight', /* 说明高亮区域的样式 */
+                exitOnEsc: true, /* 是否使用键盘Esc退出 */
+                exitOnOverlayClick: false, /* 是否允许点击空白处退出 */
+                keyboardNavigation: true, /* 是否允许键盘来操作 */
+                showBullets: false, /* 是否使用点显示进度 */
+                showProgress: false, /* 是否显示进度条 */
+                scrollToElement: true, /* 是否滑动到高亮的区域 */
+                overlayOpacity: 0.5, // 遮罩层的透明度 0-1之间
+                // positionPrecedence: ['bottom', 'top', 'right', 'left'], /* 当位置选择自动的时候,位置排列的优先级 */
+                disableInteraction: false, /* 是否禁止与元素的相互关联 */
+                hidePrev: true, /* 是否在第一步隐藏上一步 */
+                hidePrev: false,       // 在第一步中是否隐藏上一个按钮
+                hideNext: false,       // 在最后一步中是否隐藏下一个按钮
+                exitOnOverlayClick: false,  // 点击叠加层时是否退出介绍
+                showStepNumbers: false,     // 是否显示红色圆圈的步骤编号
+                disableInteraction: true,   // 是否禁用与突出显示的框内的元素的交互,就是禁止点击
+                showBullets: true,        // 是否显示面板指示点
+                // 配置内容 steps数组,内部一个对象代表一个步骤
+                steps: targetDetailSteps,
+                scrollToElement: true, // 自动滚动到目标元素
+            }).onexit((e) => {
+                that.endGuide();
+            }).oncomplete((e) => {
+                that.endGuide();
+            }).onbeforechange((e) => {
+                
+                if (e && e.className) {
+                    if (e.className.includes('switch-box')) { 
+                        that.autoBySub = false
+                    }
+
+                    if (e.className.includes('introjsFloatingElement')) {
+                        that.targetDetail.planModels = planModels
+                    }
+                }
+            }).start()
+        },
+
+
+        endGuide() {
+            introJs().setOptions({
+                steps: [
+                    {
+                        element: '#startTour',
+                        intro: '教程已结束。您可以在点击这里重新开始教程。'
+                    }
+                ],
+                overlayOpacity: 0.5, // 遮罩层的透明度 0-1之间
+                showBullets: false,
+                showProgress: false,
+                showStepNumbers: false,
+                tooltipClass: 'intro-tooltip', /* 引导说明文本框的样式 */
+                highlightClass: 'intro-highlight', /* 说明高亮区域的样式 */
+                exitOnEsc: true, /* 是否使用键盘Esc退出 */
+                exitOnOverlayClick: false, /* 是否允许点击空白处退出 */
+                scrollToElement: true, // 自动滚动到目标元素
+                skipLabel: '知道了',
+                doneLabel: '知道了'
+
+            }).onexit((e) => {
+                this.getDetails();
+            }).oncomplete((e) => {
+                this.getDetails();
+            }).onbeforechange((e) => {
+            }).start()
+        },
+
+        goPage() {
+            let { from, scopeId, current, cycleType } = this.$route.query
+            this.$router.push({ name: from, query: { scopeId: scopeId.toString(), current: current || '', cycleType } })
+        },
+
+        openTableData() {
+            this.dialogVisible = true
+        },
+
+        closeDialog(data) {
+            this.getDetails()
+        },
+
+        exportEvent() {
+            exportArrayToExcel(
+                this.$refs.xTable.getColumns(),
+                this.$refs.xTable.getData(),
+                this.targetDetail.name + '-' + this.ownerName + '.xlsx'
+            )
+        },
+
+        // 行级 + 列级 自由控制
+        checkCell({ row, column }) {
+            const scopeId = column.property
+            // 1. 把 values 里的值同步到行上的动态字段
+            const item = row.values.find(v => v.scopeId == scopeId)
+            if (item) row[scopeId] = item.value || 0
+            // 例:整行只读
+            if (!row.allowEdit) return false
+            // 其余放行
+            return true
+        },
+
+        changeTab(index) {
+            this.current = index
+            this.currentTab = this.tabs[index].name
+        },
+
+        openTargetSearch() {
+            this.checkList = this.targetDetail.krId
+            this.showTargetSearch = true
+        },
+
+
+        updateTargetDetail(data) {
+            this.targetDetail = data
+            this.initEditStatus()
+        },
+
+        confirmSelectedOkr(krIds, oIds) {
+            let url = `/okr/og/goals/bind/${this.user_info.site_id}`
+            let data = {
+                goalId: this.targetDetail.goalId,
+                objectId: oIds.toString(),
+                krId: krIds.toString(),
+            }
+
+            this.$http.post(url, data).then(res => {
+                if (res.code == 1) {
+                    this.$message.success('绑定成功')
+                    this.getDetails();
+                } 
+                else {
+                    return this.$message.error(res.message || '操作失败')
+                } 
+            })
+        },
+
+        
+        async getDetails() {
+            this.loading = true
+            let url = `/okr/og/goals/info/${this.user_info.site_id}/${this.goalId}`;
+            let res = await this.$axiosUser('get', url)
+            this.loading = false
+            this.targetDetail = res.data.data
+            let quantifyModels = this.targetDetail && this.targetDetail.quantifyModels || []
+            let { autoBySub } = quantifyModels.find(item => item.code === 'PROJECTED_VALUE')
+            this.autoBySub = autoBySub
+            if (this.targetDetail && this.targetDetail.quantifyModels.length > 0 && this.targetDetail.planModels) {
+                let obj = {
+                    code: "DIFF_VALUE",
+                    name: "差值(目标值 - 完成值)",
+                    values: []
+                }
+                let targetValues = this.targetDetail.planModels.data.find(item => item.code === "TARGET_VALUE").values.map(value => value)
+                let resultValues = this.targetDetail.planModels.data.find(item => item.code === "RESULT_VALUE").values.map(value => value)
+                targetValues.forEach((value, index) => {
+                    obj.values.push({ value: (resultValues[index].value - targetValues[index].value).toFixed(2) })
+                    // let resultValues = resultValues[index]
+                    // let targetValues = targetValues[index]
+                    // let isBothInteger = Number.isInteger(resultValues) && Number.isInteger(targetValues)
+                    // obj.values.push({ value: isBothInteger ? resultValues - targetValues : (resultValues - targetValues).toFixed(2) })
+                    // obj.values.push({ value: resultValues - targetValues })
+                })
+                this.targetDetail.planModels.data.push(obj)
+            }
+
+            this.initialPlanModels = cloneDeep(this.targetDetail.planModels)
+            
+            if (this.targetDetail.scope.cycleType == 4) {
+                if (this.targetDetail && this.targetDetail.planModels && this.targetDetail.planModels.data && this.targetDetail.planModels.data.length > 0  ) {
+                    let finalData = [];
+                    this.targetDetail.planModels.data.forEach(item => {
+                        if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
+                            finalData.push(item)
+                        }
+                    })
+                    this.targetDetail.planModels.data = finalData
+                }
+
+                console.log(this.targetDetail.planModels.data);
+                
+            }
+
+            // 导出表格数据预处理
+            const scopes = this.targetDetail && this.targetDetail.planModels && this.targetDetail.planModels.scopes || [];
+            if (scopes.length > 0)
+                this.targetDetail.planModels.data = this.flattenRowValues(this.targetDetail.planModels.data, scopes);
+
+            if (this.isAllowDismantlingTarget && this.autoBySub) {
+                this.tabs = [
+                    { id: 1, name: "概览", class: 'item-overview' },
+                    { id: 2, name: "拆解目标", class: 'item-dismantling' },
+                    { id: 3, name: "任务", class: 'item-task' },
+                    { id: 4, name: "动态", class: 'item-dynamic' }
+                ]
+            } else {
+                this.tabs = [
+                    { id: 1, name: "概览", class: 'item-overview' },
+                    { id: 3, name: "任务", class: 'item-task' },
+                    { id: 4, name: "动态", class: 'item-dynamic' }
+                ]
+            }
+
+            this.initEditStatus();
+        },
+
+        
+
+        flattenRowValues(list, scopes) {
+            list.forEach(row => {
+                scopes.forEach(s => {
+                    const hit = row.values.find(v => v.scopeId === s.scopeId);
+                    row[s.scopeId] = hit ? hit.value : '';   // 关键:把值挂到行上
+                });
+            });
+            return list;
+        },
+
+        
+
+        initEditStatus() {
+            // 根据目标状态,动态控制表格列是否可以修改
+            if (this.targetDetail && this.targetDetail.planModels && this.targetDetail.planModels.data && this.targetDetail.planModels.data.length > 0) {
+                this.targetDetail.planModels.data.forEach(item => {
+                    
+                    item.allowEdit = true
+                    if( this.targetDetail.ownerId !== this.user_info.id ) {
+                        item.allowEdit = false
+                        return
+                    }
+                    // 确认中,制作中,除了完成值,其他都可以修改
+                    if (this.targetDetail.status == 0 || this.targetDetail.status == 1) {
+                        if (item.code === 'RESULT_VALUE') item.allowEdit = false
+                        else item.allowEdit = true
+                    }
+
+                    // 执行中,可修改预估达成值,完成值
+                    if (this.targetDetail.status == 2) {
+                        if (item.code === 'RESULT_VALUE' || item.code === 'PROJECTED_VALUE') item.allowEdit = true
+                        else item.allowEdit = false
+                    }
+
+                    // 已结束,所有值都不能修改
+                    if (this.targetDetail.status == 3) {
+                        item.allowEdit = false
+                    }
+
+                    // 已结束,所有值都不能修改
+                    if (item.code == "DIFF_VALUE") {
+                        item.allowEdit = false
+                    }
+                })
+            } 
+        },
+
+       async getStatisticsData() {
+            let url = `/okr/og/goals/plan/statistics/${this.user_info.site_id}/${this.targetDetail.goalId}`
+            let res = await this.$axiosUser('get', url)
+            this.rawData = res.data.data
+            // let obj = {
+            //     code: "DIFF_VALUE",
+            //     name: "差值(目标值 - 完成值)",
+            //     values: []
+            // }
+            // if (this.isHaveQuarterModelData) {
+            //     let targetValues = this.rawData.quarterModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+            //     let resultValues = this.rawData.quarterModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+            //     targetValues.forEach((value, index) => {
+            //         let resultValues = resultValues[index].value
+            //         let targetValues = targetValues[index].value
+            //         let isBothInteger = Number.isInteger(resultValues) && Number.isInteger(targetValues)
+            //         obj.values.push({ value: isBothInteger ? resultValues - targetValues : (resultValues - targetValues).toFixed(2) })
+            //     })
+            //     this.rawData.quarterModel.data.push(obj)
+            //     obj = {
+            //         code: "DIFF_VALUE",
+            //         name: "差值(目标值 - 完成值)",
+            //         values: []
+            //     }
+            // }
+
+            // if (this.isHaveMonthModelData) {
+            //     let targetValues = this.rawData.monthModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+            //     let resultValues = this.rawData.monthModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+            //     targetValues.forEach((value, index) => {
+            //         let resultValues = resultValues[index].value
+            //         let targetValues = targetValues[index].value
+            //         let isBothInteger = Number.isInteger(resultValues) && Number.isInteger(targetValues)
+            //         obj.values.push({ value: isBothInteger ? resultValues - targetValues : (resultValues - targetValues).toFixed(2) })
+            //     })
+            //     this.rawData.monthModel.data.push(obj)
+            //     obj = {
+            //         code: "DIFF_VALUE",
+            //         name: "差值(目标值 - 完成值)",
+            //         values: []
+            //     }
+            // }
+
+            // if (this.isHaveDayModelData) {
+            //     let targetValues = this.rawData.dayModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+            //     let resultValues = this.rawData.dayModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+            //     targetValues.forEach((value, index) => {
+            //         let resultValues = resultValues[index].value
+            //         let targetValues = targetValues[index].value
+            //         let isBothInteger = Number.isInteger(resultValues) && Number.isInteger(targetValues)
+            //         obj.values.push({ value: isBothInteger ? resultValues - targetValues : (resultValues - targetValues).toFixed(2) })
+            //     })
+            //     this.rawData.dayModel.data.push(obj)
+            //     obj = {
+            //         code: "DIFF_VALUE",
+            //         name: "差值(目标值 - 完成值)",
+            //         values: []
+            //     }
+            //     if (this.targetDetail.scope.cycleType == 4) {
+            //         let finalData = [];
+            //         this.rawData.dayModel.data.forEach(item => {
+            //             if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
+            //                 finalData.push(item)
+            //             }
+            //         })
+            //         this.rawData.dayModel.data = finalData
+            //     }
+
+           // }
+
+           let obj = {
+               code: "DIFF_VALUE",
+               name: "差值(目标值 - 完成值)",
+               values: []
+           }
+           if (this.isHaveQuarterModelData) {
+               let targetValues = this.rawData.quarterModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+               let resultValues = this.rawData.quarterModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+               targetValues.forEach((value, index) => {
+                   obj.values.push({ value: (resultValues[index].value - targetValues[index].value) })
+               })
+               this.rawData.quarterModel.data.push(obj)
+               obj = {
+                   code: "DIFF_VALUE",
+                   name: "差值(目标值 - 完成值)",
+                   values: []
+               }
+           }
+
+           if (this.isHaveMonthModelData) {
+               let targetValues = this.rawData.monthModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+               let resultValues = this.rawData.monthModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+               targetValues.forEach((value, index) => {
+                   obj.values.push({ value: (resultValues[index].value - targetValues[index].value) })
+               })
+               this.rawData.monthModel.data.push(obj)
+               obj = {
+                   code: "DIFF_VALUE",
+                   name: "差值(目标值 - 完成值)",
+                   values: []
+               }
+           }
+
+           if (this.isHaveDayModelData) {
+               let targetValues = this.rawData.dayModel.data.find(item => item.code === "TARGET_VALUE").values.map(value => value.value)
+               let resultValues = this.rawData.dayModel.data.find(item => item.code === "RESULT_VALUE").values.map(value => value.value)
+               targetValues.forEach((value, index) => {
+                   obj.values.push({ value: resultValues[index] - targetValues[index] })
+               })
+               this.rawData.dayModel.data.push(obj)
+               obj = {
+                   code: "DIFF_VALUE",
+                   name: "差值(目标值 - 完成值)",
+                   values: []
+               }
+               let finalData = [];
+               this.rawData.dayModel.data.forEach(item => {
+                   if (item.code === 'TARGET_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'DIFF_VALUE') {
+                       finalData.push(item)
+                   }
+               })
+               this.rawData.dayModel.data = finalData
+           }
+        },
+
+
+        // 按日,月,季规划计划
+        planByDate(date) {
+            let url = `/okr/og/goals/plan/${date}/${this.user_info.site_id}/${this.goalId}`;
+            this.$axiosUser('post', url).then(res => {
+                if (res.data.code == 1) {
+                    this.getDetails();
+                } else {
+                    this.$message.error(res.message || '操作失败')
+                }
+            })
+        },
+
+        
+        // 删除计划
+        deletePlan() {
+            this.$confirm('删除后,所有时间计划数据将被清空,是否继续?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+
+                let url = `/okr/og/goals/plan/remove/${this.user_info.site_id}/${this.goalId}`
+                this.$axiosUser('post', url).then(res => {
+                    if (res.data.code = 1) {
+                        thi.getDetails();
+                    } else {
+                        this.$message.error(res.message || '操作失败')
+                    }
+                })
+                
+            }).catch(() => {
+                this.$message.info('已取消删除')
+            });
+            
+        },
+
+        handleEditActived({ row, column }) {
+            // 记录初始值
+            // this.initialValue = row.name;
+            if (!row.allowEdit) return;
+
+            const scopeId = column.field;
+            const valueObj = row.values.find(v => v.scopeId === scopeId);
+            if (valueObj) {
+                const key = `${scopeId}`;
+                this.editInitialMap[key] = valueObj.value
+                // this.$set(this.editInitialMap, key, valueObj.value);
+            }
+        },
+
+        // 单元格编辑完成的回调
+        async handleEditClosed(e) {
+            if (!e) return
+            let prop = e.column.field
+            let code = e.row.code
+            let value = e.row[prop] == '' || e.row[prop] == null ? 0 : e.row[prop]
+            let { valueId } = e.row.values.find(value => value.scopeId == prop)
+            e.row.values.find(value => value.scopeId == prop).value = value
+
+            const valueObj = e.row.values.find(v => v.scopeId == prop);
+            const key = `${prop}`;
+            const initial = this.editInitialMap[key];
+            const current = valueObj.value;
+
+
+            if (current == null || current == '') {
+                valueObj.value = initial; // 还原
+                return
+            }
+
+            let res;
+            if (e.row.name === '完成值') {
+                res = await this.handleEditProjectedValue(value, valueId, this.goalId, 'result')
+                
+            } else if (e.row.name.includes('预估')) {
+                res = await this.handleEditProjectedValue(value, valueId, this.goalId, 'projected')
+            } else {
+                res = await this.handleEditOtherValue(code, value, valueId, this.goalId)
+            }
+
+            if (res.code == 1) {
+                this.getDetails()
+                // 清理缓存
+                this.$delete(this.editInitialMap, key);
+            }
+            else {
+                return this.$message.error(res.message || '操作失败')
+            }
+            
+        },
+
+
+        // 修改目标预计达成值,完成值
+        async handleEditProjectedValue(value, valueId, goalId, editValue) {
+            let url = `/okr/og/goals/plan/${editValue}/${this.user_info.site_id}`
+            let data = {
+                goalId,
+                valueId,
+                value
+            }
+            try {
+                // 直接把响应体当成返回值
+                const res = await this.$http.post(url, data)
+                return res          // ← 这就是“请求的结果作为返回值”
+            } catch (e) {
+                this.$message.error(e.message || '保存失败')
+                throw e                  // 继续向上抛,方便调用方捕获
+            }
+
+            
+        },
+
+        // 修改红线值,挑战值,目标值,超越值 
+        async handleEditOtherValue(code, value, valueId, goalId) {
+            let url = `/okr/og/goals/plan/base/${this.user_info.site_id}`
+            let data = {
+                goalId,
+                value,
+                valueId,
+                code
+            }
+            try {
+                // 直接把响应体当成返回值
+                const res = await this.$http.post(url, data)
+                return res          // ← 这就是“请求的结果作为返回值”
+            } catch (e) {
+                this.$message.error(e.message || '保存失败')
+                throw e                  // 继续向上抛,方便调用方捕获
+            }
+            
+        },
+
+        // 平分数据
+        svgTargetNums() {
+            // 平分区间
+            let range = this.targetDetail.planModels.scopes.length || 0
+            // 红线值 - 目标数
+            let riskValueTarget = this.targetDetail.quantifyModels.find(item => item.code === 'RISK_VALUE').value
+            // 目标值 - 目标数
+            let targetValueTarget = this.targetDetail.quantifyModels.find(item => item.code === 'TARGET_VALUE').value
+            // 挑战值 - 目标数
+            let challengeValueTarget = this.targetDetail.quantifyModels.find(item => item.code === 'CHALLENGE_VALUE').value
+            // 超越值 - 目标数
+            let surpassValueTarget = this.targetDetail.quantifyModels.find(item => item.code === 'SURPASS_VALUE').value
+            // 预估达成值 - 目标数
+            let projectValueTarget = this.targetDetail.quantifyModels.find(item => item.code === 'PROJECTED_VALUE').value
+            // 如果目标数为0,返回一个全是0的数组
+            let zeroArray = Array.from({ length: range }, (v, i) => 0)
+            // 平分红线值
+            let riskValues = riskValueTarget !== 0 ? splitTarget(riskValueTarget, range) : zeroArray;
+            // 平分目标值
+            let targetValues = targetValueTarget !== 0 ? splitTarget(targetValueTarget, range) : zeroArray;
+            // 平分挑战值
+            let challengeValues = challengeValueTarget !== 0 ? splitTarget(challengeValueTarget, range) : zeroArray;
+            // 平分超越值
+            let surpassValues = surpassValueTarget !== 0 ? splitTarget(surpassValueTarget, range) : zeroArray;
+            // 平分预估达成值
+            let projectValues = projectValueTarget !== 0 ? splitTarget(projectValueTarget, range) : zeroArray;
+            let svgTargetData = []
+
+            this.initialPlanModels.data.forEach((item) => {
+                if (item.code === "RISK_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: riskValues[index] }))
+                }
+                if (item.code === "TARGET_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: targetValues[index] }))
+                }
+                if (item.code === "CHALLENGE_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: challengeValues[index] }))
+                }
+
+                if (item.code === "SURPASS_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: surpassValues[index] }))
+                }
+
+                if (item.code === "PROJECTED_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: projectValues[index] }))
+                }
+                if (item.code !== "DIFF_VALUE") svgTargetData.push(item)
+            })
+
+            this.targetDetail.planModels.data.forEach((item) => {
+                if (item.code === "RISK_VALUE") {
+                    item.values = item.values.map((value, index) => ({...value, value: riskValues[index]}))
+                }
+                if (item.code === "TARGET_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: targetValues[index] }))
+                }
+                if (item.code === "CHALLENGE_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: challengeValues[index] }))
+                }
+
+                if (item.code === "SURPASS_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: surpassValues[index] }))
+                }
+
+                if (item.code === "PROJECTED_VALUE") {
+                    item.values = item.values.map((value, index) => ({ ...value, value: projectValues[index] }))
+                }
+                if (item.code !== "DIFF_VALUE") svgTargetData.push(item)
+            })
+            this.openSvgTarget = false;
+            this.svgTargetData = cloneDeep(this.targetDetail.planModels)
+            this.svgTargetData.data = this.svgTargetData.data.filter(item => item.code !== 'DIFF_VALUE')
+
+            // const scopeLen = this.targetDetail.planModels.scopes.length || 0
+            // // 一次性提取所有目标值
+            // const targetMap = new Map(
+            //     this.targetDetail.quantifyModels
+            //     .filter(m => ['红线值', '目标值', '挑战值', '超越值', '预估达成值'].includes(m.name))
+            //     .map(m => [m.name, m.value])
+            // )
+
+            // // 预生成所有字段的均分数组(0 则全 0)
+            // const splitMap = new Map(
+            //     ['红线值', '目标值', '挑战值', '超越值', '预估达成值'].map(key => [
+            //         key,
+            //         targetMap.get(key) !== 0 ? splitTarget(targetMap.get(key) || 0, scopeLen) : Array.from({ length: scopeLen }, () => 0)
+            //     ])
+            // )
+            
+
+            // // 一次循环赋值
+            // this.svgTargetData.data.forEach(item => {
+            //     const arr = splitMap.get(item.name)
+            //     if (arr) {
+            //         item.values = item.values.map((v, i) => ({ ...v, value: arr[i] }))
+            //     }
+            // })
+
+            this.openSvgTargetDialog = true
+        },
+
+        saveSvgTargetNums() {
+            let url = `/okr/og/goals/plan/base/multi/${this.user_info.site_id}/${this.goalId}`
+            this.initialPlanModels.data = this.initialPlanModels.data.filter(item => item.code !== 'DIFF_VALUE' || item.code === 'RESULT_VALUE' || item.code === 'TARGET_VALUE')
+            let data = { ...this.initialPlanModels }
+            console.log(data)
+            this.$http.post(url, data).then(res => {
+                if (res.code == 1) {
+                    this.openSvgTargetDialog = false
+                    this.openSvgTarget = true;
+                    this.$message.success("操作成功")
+                    this.getDetails();
+                }
+                else {
+                    return this.$message.error(res.message || '操作失败')
+                }
+            })
+        },
+
+        resetData() {
+            this.openSvgTarget = true;
+            this.openSvgTargetDialog = false
+            this.getDetails();
+        },
+
+        getDataCenter() {
+            this.goalId = this.targetDetail.goalId
+            this.title = this.targetDetail.name + '-' + this.ownerName
+            this.goalList = [{ 
+                goalId: this.targetDetail.goalId,
+                name: this.targetDetail.name,
+                ownerName: this.ownerName
+            }];
+            this.dataCenterDialogVisible = true
+        },
+
+        /* 拦截浏览器返回键 */
+        blockPopstate(e) {
+            // 阻止默认返回行为
+            e.preventDefault();
+            // 可以选择关闭 dialog
+            this.cycleTableDialogVisible = false;
+            this.dataCenterDialogVisible = false;
+            // 重新插入一条记录,防止再次返回
+            history.pushState(null, null, location.href);
+        },
+
+        handleClose() {
+            this.dataCenterDialogVisible = false;
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        onDialogOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+            // 延迟200ms,等待全屏动画彻底结束
+            setTimeout(() => {
+                this.outerRefresh += 1
+            }, 200)
+        },
+
+
+        handleCycleTableClose() {
+            this.cycleTableDialogVisible = false;
+        },
+
+        /* 开启 dialog 时:禁用返回键 */
+        handleCycleTableOpen() {
+            // 立即往 history 里插一条空记录,拦截返回
+            history.pushState(null, null, location.href);
+            // 监听 popstate
+            window.addEventListener('popstate', this.blockPopstate, false);
+        },
+
+    }
+}
+</script>
+
+<style lang="scss">
+
+/* 让标题与按钮在同一行,按钮靠右 */
+.alert-title {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+
+.all {
+    
+
+    .el-switch__core {
+        width: 30px !important;
+        height: 16px;
+    }
+
+    .el-switch__core::after {
+        width: 14px;
+        height: 14px;
+        margin-top: -1px;
+    }
+
+    .el-switch.is-checked .el-switch__core::after {
+        margin-left: -15px;
+    }
+
+    .multi-cycle-table {
+
+        .el-dialog__body {
+            width: 100%;
+            height: 100%;
+            padding: 0;
+        }
+    }
+
+    .data-center-box {
+        .el-dialog__header {
+            display: none;
+        }
+
+        .el-dialog__body {
+            width: 100%;
+            height: 100%;
+            padding: 0;
+        }
+    }
+}
+</style>
+
+<style scoped lang="scss">
+
+.color-green { color: #2ecc71; }
+.color-red { color: #e74c3c; }
+
+.line {
+    width: 4px;
+    height: 20px;
+    background: #1299f9;
+    margin-right: 10px;
+}
+
+.base-table {
+    width: 100%;
+    border-collapse: collapse;
+    margin: 10px auto;
+    border-right: 1px solid #ddd;
+    border-top: 1px solid #ddd;
+}
+
+.base-table th,
+.base-table td {
+    padding: 3px 6px;
+    text-align: center;
+    border-bottom: 1px solid #ddd;
+    box-sizing: border-box;
+}
+
+.base-table th {
+    background-color: #f2f2f2;
+    font-weight: bold;
+}
+
+.base-table td {
+    border-left: 1px solid #ccc;
+}
+
+.base-table tr:hover {
+    background-color: #f5f5f5;
+}
+
+.all {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    
+    .PageHead {
+            height: 42px;
+            font-size: 14px;
+            
+            span {
+                height: 32px;
+                line-height: 32px;
+            }
+    
+            span:nth-child(1) {
+                cursor: pointer;
+                width: 42px;
+                color: #8f8f8f;
+                position: relative;
+            }
+    
+            span:nth-child(1)::after {
+                position: absolute;
+                content: " ";
+                height: 18px;
+                width: 1px;
+                background-color: #8f8f8f;
+                right: 0px;
+                top: 50%;
+                margin-top: -9px;
+            }
+    
+            span:nth-child(1):hover {
+                color: #1299f9;
+            }
+    
+            span:nth-child(2) {
+                padding-left: 12px;
+                color: #585858;
+            }
+    
+            .slot {
+                margin-left: 12px;
+            }
+        }
+
+    .top {
+        width: 100%;
+        background-color: #fff;
+        border-radius: 6px;
+
+        .icon {
+            width: 36px;
+            height: 36px;
+            border: 1px solid #ccc;
+            border-radius: 50%;
+            font-size: 20px;
+            color: #999;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            transition: all 0.3s;
+            margin-left: 10px;
+    
+            &:hover {
+                cursor: pointer;
+                background: #f5f5f5;
+            }
+        }
+    }
+
+    .bottom {
+        width: 100%;
+        background-color: #fff;
+        border-radius: 6px;
+
+        .cursor-pointer {
+            display: flex !important;
+            text-align: left !important;
+    
+            &:hover {
+                cursor: pointer;
+                color: #409eff;
+            }
+        }
+    }
+
+    .target-info {
+        width: 100%;
+        height: 140px;
+        padding: 0 10px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .type-img-box {
+            width: 60px;
+            height: 60px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 50%;
+            color: white;
+            font-size: 30px;
+            background-color: #409EFF;
+            margin-right: 10px;
+
+        }
+
+        .name {
+            font-size: 20px;
+            font-weight: 600;
+            margin: 5px 0;
+
+            
+        }
+
+        .tag {
+            box-sizing: border-box;
+            margin-right: 10px;
+            color: #999;
+        }
+
+        .chart1 {
+            position: relative;
+
+        }
+
+        .switch-box {
+            display: flex;
+            margin-top: 5px;
+            // right: 100px;
+            // top: 80px;
+        }
+    }
+
+
+    .tab-list {
+        display: flex;
+        align-items: center;
+        margin: 0 0 10px 0;
+        border-bottom: 1px solid #eee;
+        .tab-item {
+            padding: 0 10px;
+            height: 40px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            position: relative;
+            &:hover {
+                background-color: #f5f5f5;
+                cursor: pointer;
+            }
+            &.active {
+                font-size: 14px;
+                color: #409EFF;
+                font-weight: bolder;
+            }
+
+            .line {
+                width: 30px;
+                height: 3px;
+                border-radius: 6px;
+                background: #409EFF;
+                position: absolute;
+                bottom: 0px;
+                left: 50%;
+                transform: translateX(-50%);
+            }
+        }
+    }
+
+    .card-list {
+        gap: 12px;
+        padding: 0 12px;
+        box-sizing: border-box;
+
+        .card-item {
+            width: 20%;
+            height: 100px;
+            border-radius: 12px;
+            background-color: #409EFF;
+            padding: 10px;
+            box-sizing: border-box;
+
+            .card-title {
+                font-weight: 500;
+                font-size: 14px;
+                color: #999;
+            }
+
+            .card-num {
+                margin-top: 20px;
+                font-weight: 600;
+                font-size: 20px;
+            }
+        }
+
+        & .card-item:nth-child(1) {
+            background: #FFEEEB;
+        }
+
+        & .card-item:nth-child(2) {
+            background: #E9F8F7;
+        }
+
+        & .card-item:nth-child(3) {
+            background: #FCF9ED;
+        }
+
+        & .card-item:nth-child(4) {
+            background: #FFF7EE;
+        }
+
+        & .card-item:nth-child(5) {
+            background: #F3F3FE;
+        }
+
+        & .card-item:nth-child(6) {
+            background: #c6f7de;
+        }
+    }
+
+    .top {
+        display: flex;
+        flex-direction: column;
+    }
+
+    .h-100 {
+        height: 100%;
+    }
+
+
+    .bottom {
+        display: flex;
+        flex-direction: column;
+        margin-top: 20px;
+        .title-box {
+            width: 100;
+            height: 50px;
+            padding: 0 10px;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .title {
+                margin-right: 10px;
+                font-weight: 600;
+            }
+        }
+
+        .table-box {
+            flex: 1;
+            width: 100%;
+            padding: 0 20px;
+            box-sizing: border-box;
+            overflow-y: auto;
+
+            /* 设置滚动条的宽度和背景色 */
+            &::-webkit-scrollbar {
+                width: 6px !important;
+                height: 6px !important;
+                background-color: #f9f9f9 !important;
+            }
+
+            /* 设置滚动条滑块的样式 */
+            &::-webkit-scrollbar-thumb {
+                border-radius: 6px !important;
+                background-color: #c1c1c1 !important;
+            }
+
+            /* 设置滚动条滑块hover样式 */
+            &::-webkit-scrollbar-thumb:hover {
+                background-color: #a8a8a8 !important;
+            }
+
+            /* 设置滚动条轨道的样式 */
+            &::-webkit-scrollbar-track {
+                box-shadow: inset 0 0 5px rgba(87, 175, 187, 0.1) !important;
+                border-radius: 6px !important;
+                background: #ededed !important;
+            }
+
+        }
+
+    }
+}
+</style>
+
+<style lang="scss">
+.introjs-helperLayer {
+    box-shadow: rgba(33, 33, 33, 0.8) 0px 0px 1px 0px, rgba(33, 33, 33, 0.5) 0px 0px 0px 5000px !important;
+    border: 3px dashed #409eff;
+}
+
+/* 调整 intro.js 弹出框的大小 */
+.introjs-tooltip {
+    width: auto;
+    /* 自动调整宽度 */
+    max-width: 1600px;
+    /* 最大宽度 */
+    height: auto;
+    /* 自动调整高度 */
+    overflow: hidden;
+    /* 防止内容溢出 */
+}
+
+.new-tips {
+    color: #409eff;
+    line-height: 80px;
+    cursor: pointer;
+}
+
+.introjs-tooltip-title {
+    font-size: 16px;
+    width: 80%;
+    padding-top: 10px;
+}
+
+.warper {
+    width: 200px;
+    height: 100px;
+    line-height: 100px;
+    text-align: center;
+    border: 1px solid saddlebrown;
+}
+
+/* 重置引导组件样式(类似element-ui个人使用) */
+.intro-tooltip {
+    color: #ffff;
+    background: #2c3e50;
+}
+
+/* 引导提示框的位置 */
+.introjs-bottom-left-aligned {
+    left: 45% !important;
+}
+
+.introjs-right,
+.introjs-left {
+    top: 30%;
+}
+
+.intro-highlight {
+    background: rgba(255, 255, 255, 0.5);
+}
+
+.introjs-arrow.left {
+    border-right-color: #2c3e50;
+}
+
+.introjs-arrow.top {
+    border-bottom-color: #2c3e50;
+}
+
+.introjs-arrow.right {
+    border-left-color: #2c3e50;
+}
+
+.introjs-arrow.bottom {
+    border-top-color: #2c3e50;
+}
+
+/* 提示框头部区域 */
+.introjs-tooltip-header {
+    padding-right: 0 !important;
+    padding-top: 0 !important;
+}
+
+.introjs-skipbutton {
+    color: #409eff !important;
+    font-size: 14px !important;
+    font-weight: normal !important;
+    //   padding: 8px 10px !important ;
+}
+
+.introjs-tooltipbuttons {
+    border: none !important;
+}
+
+.introjs-tooltiptext {
+    font-size: 14px !important;
+    padding: 15px !important;
+    box-sizing: border-box;
+}
+
+.introjs-tooltiptext .title{
+    min-width: 600px;
+}
+
+/* 提示框按钮 */
+.introjs-tooltipbuttons {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.introjs-button {
+    width: 50px !important;
+    text-align: center;
+    padding: 4px !important;
+    font-size: 12px !important;
+    font-weight: 500 !important;
+    border-radius: 3px !important;
+    border: none !important;
+}
+
+.introjs-button:last-child {
+    margin-left: 10px;
+}
+
+.introjs-prevbutton {
+    color: #606266 !important;
+    background: #fff !important;
+    border: 1px solid #dcdfe6 !important;
+}
+
+.introjs-nextbutton {
+    color: #fff !important;
+    background-color: #409eff !important;
+    border-color: #409eff !important;
+}
+
+.introjs-disabled {
+    color: #9e9e9e !important;
+    border-color: #bdbdbd !important;
+    background-color: #f4f4f4 !important;
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 406 - 176
src/okr/views/targetBusiness/targetDetail.vue


+ 5 - 2
src/okr/views/targetBusiness/utils/step/targetDetailSteps.js

@@ -51,8 +51,11 @@ export default [
         element: '.table-box', // 元素
         title: "按月度规划目标",
         intro: `<div class=\'title\'>自由编辑目标值,红线值,挑战值,超越值,预估达成值</div>
-        <div>支持一键平均分配</div>`, // 内容
-        position: "bottom"
+        <div>支持一键平均分配</div>
+        <div style="display: flex; margin-top: 10px;">
+        <img src="` + monthData + `" alt=""  style="width: 1000px; "/>
+        </div>`, // 内容
+        position: "top"
     },
     
 

+ 54 - 0
src/store/modules/flowDesigner.js

@@ -0,0 +1,54 @@
+// 初始状态抽离出来,方便重置
+const getDefaultState = () => ({
+    flowId: '',
+    isTried: false,
+    promoterDrawer: false,
+    flowPermission1: {},
+    approverDrawer: false,
+    approverConfig1: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    conditionDrawer: false,
+    conditionsConfig1: {
+        conditionNodes: []
+    }
+})
+
+const state = getDefaultState()
+
+const mutations = {
+    SET_FLOW_ID: (state, payload) => { state.flowId = payload },
+    SET_IS_TRIED: (state, payload) => { state.isTried = payload },
+    SET_PROMOTER: (state, payload) => { state.promoterDrawer = payload },
+    SET_FLOW_PERMISSION: (state, payload) => { state.flowPermission1 = payload },
+    SET_APPROVER: (state, payload) => { state.approverDrawer = payload },
+    SET_APPROVER_CONFIG: (state, payload) => { state.approverConfig1 = payload },
+    SET_COPYER: (state, payload) => { state.copyerDrawer = payload },
+    SET_COPYER_CONFIG: (state, payload) => { state.copyerConfig1 = payload },
+    SET_CONDITION: (state, payload) => { state.conditionDrawer = payload },
+    SET_CONDITIONS_CONFIG: (state, payload) => { state.conditionsConfig1 = payload },
+
+    // 可选:一键重置状态
+    RESET_STATE: state => Object.assign(state, getDefaultState())
+}
+
+const actions = {
+    setFlowId({ commit }, payload) { commit('SET_FLOW_ID', payload) },
+    setIsTried({ commit }, payload) { commit('SET_IS_TRIED', payload) },
+    setPromoter({ commit }, payload) { commit(' SET_PROMOTER', payload) },
+    setFlowPermission({ commit }, payload) { commit('SET_FLOW_PERMISSION', payload) },
+    setApprover({ commit }, payload) { commit('SET_APPROVER', payload) },
+    setApproverConfig({ commit }, payload) { commit('SET_APPROVER_CONFIG', payload) },
+    setCopyer({ commit }, payload) { commit('SET_COPYER', payload) },
+    setCopyerConfig({ commit }, payload) { commit('SET_COPYER_CONFIG', payload) },
+    setCondition({ commit }, payload) { commit('SET_CONDITION', payload) },
+    setConditionsConfig({ commit }, payload) { commit('SET_CONDITIONS_CONFIG', payload) },
+    resetState({ commit }) { commit('RESET_STATE') }
+}
+
+export default {
+    namespaced: true, // 开启命名空间,避免冲突
+    state,
+    mutations,
+    actions
+}

+ 236 - 0
src/store/modules/workflow.js

@@ -0,0 +1,236 @@
+// 初始状态抽离出来,方便重置
+const getDefaultState = () => ({
+    nodeMap: new Map(),
+    isEdit: null,
+    selectedNode: {},
+    selectFormItem: null,
+    design: {
+        "formId": "78606156432c441a977147250481c72b",
+        "formName": "未命名表单123",
+        "logo": {
+            "icon": "el-icon-eleme",
+            "background": "#1e90ff"
+        },
+        "settings": {
+            "sign": false,
+            "admin": [
+                {
+                    "id": 381496,
+                    "name": "旅人",
+                    "type": "user",
+                    "selected": false
+                }
+            ],
+            "notify": {
+                "title": "消息通知标题",
+                "types": [
+                    "APP",
+                    {
+                        "name": "应用内通知",
+                        "type": "APP"
+                    }
+                ]
+            },
+            "commiter": []
+        },
+        "groupId": 820,
+        "formItems": [
+            {
+                "id": "field6861239319389",
+                "icon": "el-icon-more-outline",
+                "name": "TextareaInput",
+                "props": {
+                    "required": false,
+                    "enablePrint": true
+                },
+                "title": "多行文本输入",
+                "value": "",
+                "valueType": "String"
+            },
+            {
+                "id": "field8475439321176",
+                "icon": "el-icon-edit-outline",
+                "name": "NumberInput",
+                "props": {
+                    "required": false,
+                    "enablePrint": true
+                },
+                "title": "数字输入框",
+                "value": "",
+                "valueType": "Number"
+            },
+            {
+                "id": "field2471339323216",
+                "icon": "el-icon-circle-check",
+                "name": "SelectInput",
+                "props": {
+                    "options": [
+                        "选项1",
+                        "选项2"
+                    ],
+                    "required": false,
+                    "expanding": false,
+                    "enablePrint": true
+                },
+                "title": "单选框",
+                "value": "",
+                "valueType": "String"
+            },
+            {
+                "id": "field3264839325443",
+                "icon": "el-icon-user",
+                "name": "UserPicker",
+                "props": {
+                    "multiple": false,
+                    "required": false,
+                    "enablePrint": true
+                },
+                "title": "人员选择",
+                "value": [],
+                "valueType": "User"
+            }
+        ],
+        "process": {
+            "id": "root",
+            "desc": "任何人",
+            "name": "发起人",
+            "type": "ROOT",
+            "props": {
+                "formPerms": [
+                    {
+                        "id": "field6861239319389",
+                        "perm": "E",
+                        "title": "多行文本输入",
+                        "required": false
+                    },
+                    {
+                        "id": "field8475439321176",
+                        "perm": "E",
+                        "title": "数字输入框",
+                        "required": false
+                    },
+                    {
+                        "id": "field2471339323216",
+                        "perm": "E",
+                        "title": "单选框",
+                        "required": false
+                    },
+                    {
+                        "id": "field3264839325443",
+                        "perm": "E",
+                        "title": "人员选择",
+                        "required": false
+                    }
+                ],
+                "assignedUser": []
+            },
+            "children": {
+                "id": "node_393470951726",
+                "name": "审批人",
+                "type": "APPROVAL",
+                "props": {
+                    "mode": "AND",
+                    "role": [],
+                    "sign": false,
+                    "leader": {
+                        "level": 1
+                    },
+                    "nobody": {
+                        "handler": "TO_PASS",
+                        "assignedUser": []
+                    },
+                    "refuse": {
+                        "type": "TO_END",
+                        "target": ""
+                    },
+                    "formUser": "",
+                    "formPerms": [
+                        {
+                            "id": "field6861239319389",
+                            "perm": "R",
+                            "title": "多行文本输入",
+                            "required": false
+                        },
+                        {
+                            "id": "field8475439321176",
+                            "perm": "R",
+                            "title": "数字输入框",
+                            "required": false
+                        },
+                        {
+                            "id": "field2471339323216",
+                            "perm": "R",
+                            "title": "单选框",
+                            "required": false
+                        },
+                        {
+                            "id": "field3264839325443",
+                            "perm": "R",
+                            "title": "人员选择",
+                            "required": false
+                        }
+                    ],
+                    "leaderTop": {
+                        "endLevel": 1,
+                        "endCondition": "TOP"
+                    },
+                    "timeLimit": {
+                        "handler": {
+                            "type": "NOTIFY",
+                            "notify": {
+                                "hour": 1,
+                                "once": false
+                            }
+                        },
+                        "timeout": {
+                            "unit": "H",
+                            "value": "01"
+                        }
+                    },
+                    "selfSelect": {
+                        "multiple": false
+                    },
+                    "assignedType": "ASSIGN_USER",
+                    "assignedUser": [
+                        {
+                            "id": 381496,
+                            "name": "旅人",
+                            "type": "user",
+                            "selected": false
+                        }
+                    ]
+                },
+                "children": {},
+                "parentId": "root"
+            },
+            "parentId": null
+        },
+        "isStop": false,
+        "isDelete": false,
+        "sort": 0,
+        "created": "2025-12-05 20:56:47",
+        "updated": "2025-12-05 20:56:47"
+    }
+})
+
+const state = getDefaultState()
+
+const mutations = {
+    selectedNode(state, val) {
+        state.selectedNode = val
+    },
+    loadForm(state, val) {
+        state.design = val
+    },
+    setIsEdit(state, val) {
+        state.isEdit = val
+    }
+}
+
+
+
+export default {
+    namespaced: true, // 开启命名空间,避免冲突
+    state,
+    mutations,
+}

+ 76 - 0
src/utils/api/design.js

@@ -0,0 +1,76 @@
+import request from './request.js'
+
+
+// 查询表单组
+export function getFormGroups(param) {
+  return request({
+    url: 'admin/form/group',
+    method: 'get',
+    params: param
+  })
+}
+
+// 表单排序
+export function groupItemsSort(param) {
+  return request({
+    url: 'admin/form/group/sort',
+    method: 'put',
+    data: param
+  })
+}
+
+// 更新表单组
+export function updateGroup(param, method) {
+  return request({
+    url: 'admin/form/group',
+    method: method,
+    params: param
+  })
+}
+
+// 获取表单分组
+export function getGroup() {
+  return request({
+    url: 'admin/form/group/list',
+    method: 'get'
+  })
+}
+
+// 更新表单
+export function updateForm(param) {
+  return request({
+    url: 'admin/form',
+    method: 'put',
+    params: param
+  })
+}
+
+export function createForm(param){
+  return request({
+    url: 'admin/form',
+    method: 'post',
+    data: param
+  })
+}
+
+// 查询表单详情
+export function getFormDetail(id) {
+  return request({
+    url: 'admin/form/detail/' + id,
+    method: 'get'
+  })
+}
+
+// 更新表单详情
+export function updateFormDetail(param) {
+  return request({
+    url: 'admin/form/detail',
+    method: 'put',
+    data: param
+  })
+}
+
+export default {
+  getFormGroups, groupItemsSort, createForm, getFormDetail,
+  updateGroup, getGroup, updateForm, updateFormDetail
+}

+ 32 - 0
src/utils/api/org.js

@@ -0,0 +1,32 @@
+import request from './request.js'
+
+
+// 查询组织架构树
+export function getOrgTree(param) {
+  return request({
+    url: 'oa/org/tree',
+    method: 'get',
+    params: param
+  })
+}
+
+// 查询系统角色
+export function getRole() {
+  return request({
+    url: 'oa/org/role',
+    method: 'get'
+  })
+}
+
+// 搜索人员
+export function getUserByName(param) {
+  return request({
+    url: 'oa/org/tree/user/search',
+    method: 'get',
+    params: param
+  })
+}
+
+export default {
+  getOrgTree, getUserByName, getRole
+}

+ 0 - 0
src/utils/api/process.js


+ 65 - 0
src/utils/api/request.js

@@ -0,0 +1,65 @@
+import Vue from "vue";
+import axios from "axios";
+
+import { Notification, MessageBox, Message } from "element-ui";
+
+// 第三方插件
+import "element-ui/lib/theme-chalk/index.css";
+
+Vue.prototype.$axios = axios;
+// 字体图标
+
+const service = axios.create({
+	baseURL: Vue.prototype.BASE_URL + ":1000",
+	timeout: 50000
+});
+
+service.defaults.withCredentials = true; // 让ajax携带cookie
+service.interceptors.request.use(
+	// 每次请求都自动携带Cookie
+	config => {
+		//config.headers.Cookie = document.cookie
+
+		return config;
+	},
+	// eslint-disable-next-line handle-callback-err
+	error => {
+		return Promise.reject(error);
+	}
+);
+
+service.interceptors.response.use(
+	rsp => {
+
+		return rsp;
+	},
+	// 拦截异常的响应
+	err => {
+		console.log("请求", err);
+		switch (err.response.status) {
+			case 401:
+				MessageBox.alert("登陆已过期,请关闭当前窗口重新进入-能臣工作台");
+				break;
+			case 403:
+				//Message.warning("抱歉,您无权访问!")
+				break;
+			case 500:
+				Notification.error({ title: "提醒", message: "服务器出了点小错误" });
+				break;
+			case 404:
+				Notification({
+					title: "提醒",
+					message: "未找到,检查参数",
+					type: "warning"
+				});
+				break;
+			default:
+				//throw 'request error'
+				return Promise.reject(err);
+		}
+		//throw 'request error'
+		return Promise.reject(err);
+	}
+);
+
+export default service;

+ 43 - 0
src/utils/findDeptItem.js

@@ -0,0 +1,43 @@
+/* ====== 1. 拍平整棵树 ====== */
+export function flatTree(tree) {
+    const map = new Map();
+    function dfs(node) {
+        map.set(node.id, node);
+        (node.children || []).forEach(dfs);
+    }
+    tree.forEach(dfs);
+
+    // 如果你还想拿到“根”节点本身,补一个虚拟根
+    // map.set(0, { id: 0, name: 'ROOT', pid: null, children: tree });
+    return map;
+}
+
+/* ====== 2. 查单个 id 的完整路径(根 -> 自己) ====== */
+export function getPath(id, map) {
+    const path = [];
+    let cur = map.get(id);
+    if (!cur) return [];          // 非法 id
+    while (cur) {
+        path.unshift(cur);          // 头插
+        cur = map.get(cur.pid);     // 找父亲
+    }
+    return path;
+}
+
+/* ====== 3. 查 ids 各自的路径 ====== */
+export function getPaths(ids, tree) {
+    const map = flatTree(tree);
+    return ids.map(id => {
+        const numId = Number(id);   // 统一转数字
+        return {
+            id: numId,
+            path: getPath(numId, map).map(n => ({
+                id: n.id,
+                name: n.name,
+                label: n.label
+            }))
+        };
+    });
+}
+
+

+ 72 - 0
src/utils/workflow/const.js

@@ -0,0 +1,72 @@
+/*
+ * @Date: 2023-03-29 15:25:37
+ * @LastEditors: LDH 574427343@qq.com
+ * @LastEditTime: 2023-03-29 15:52:38
+ * @FilePath: /flow-designer/src/utils/const.js
+ */
+
+export let bgColors = ["148.6,212.3,117.1", '87, 106, 149', '121.3, 187.1, 255', '51.2, 126.4, 204', '255, 148, 62', '50, 150, 250', '50, 150, 250']
+
+export let nodeTypeList = ["未知", "发起人", "网关", "条件", "审核人", "抄送人", "抄送人",'审核人'];
+
+export let setTypes = [
+  { value: 1, label: '指定成员' },
+  { value: 2, label: '主管' },
+  { value: 3, label: '指定角色' },
+  //{ value: 4, label: '指定部门' },
+  { value: 5, label: '发起人自己' },
+  { value: 6, label: '层层审批' },
+]
+
+export let optTypes = [
+  { value: '1', label: '小于' },
+  { value: '2', label: '大于' },
+  { value: '3', label: '小于等于' },
+  { value: '4', label: '等于' },
+  { value: '5', label: '大于等于' },
+  { value: '6', label: '介于两个数之间' },
+]
+
+export let opt1s = [
+  { value: '<', label: '<' },
+  { value: '≤', label: '≤' },
+]
+
+
+export let approveList = {
+  1: '张三',
+  2: '李四',
+  3: '王五',
+  4: '菜六',
+  5: '牛七',
+  6: '马八',
+  7: '李九',
+  8: '周十',
+  9: '肖十一',
+  10: '令狐冲',
+  11: '风清扬',
+  12: '刘正风',
+  13: '岳不群',
+  14: '宁中则',
+  15: '桃谷六仙',
+  16: '不介和尚',
+  17: '丁一师太',
+  18: '依林师妹',
+  19: '邱灵珊',
+  20: '任盈盈'
+}; 
+
+export let statusColor = {
+  0: 'info', 
+  1: 'primary',//提交
+  2: 'primary',//同意
+  3: 'danger',//拒绝
+  4: 'danger',//撤回
+  5: 'danger',//作废
+  6: 'danger',//终止
+  7: 'primary',//
+  8: 'danger',//打回修改
+  9: 'primary',//加批
+  99: 'success',//处理中
+  100: 'info' 
+}; ; 

+ 58 - 0
src/utils/workflow/formatFlowPreview.js

@@ -0,0 +1,58 @@
+// import { FormatUtils } from '@/utils/formatFlowPreview' 
+const isEmptyArray = data => Array.isArray(data) ? data.length === 0 : true
+
+export class FormatUtils {
+    /**
+     * 对基础设置,高级设置等设置页内容进行格式化
+     * @param params
+     */
+    static formatSettings(param) {
+        let nodeList = [];
+        for (let node of param.bpmnNodeList) {
+            nodeList.push(this.createNewNode(node));
+        }
+        return this.depthConverterToTree(nodeList);
+    }
+
+    /**
+     * 展平树结构
+     * @param {Object} treeData  - 节点数据 
+     * @returns Array - 节点数组
+     */
+    static createNewNode(node) {
+        let newNode = {
+            nodeId: node.nodeId,
+            nodeType: node.nodeType,
+            nodeName: node.nodeName,
+            nodeDisplayName: this.arrToStr(node?.params?.assigneeList),
+            nodeTo: node.params?.nodeTo,
+            assigneeList: node?.params?.assigneeList,
+        };
+        return newNode;
+    }
+    static arrToStr(arr) {
+        if (arr) {
+            return arr.map(item => { return item.assigneeName ?? item.elementName }).toString()
+        }
+    }
+    static depthConverterToTree(parmData) {
+        if (isEmptyArray(parmData)) return
+        let nodesGroup = {}, startNode = {}
+        for (let t of parmData) {
+            if (!nodesGroup.hasOwnProperty(t.nodeId)) {
+                nodesGroup[t.nodeId] = t
+            }
+        }
+        for (let node of parmData) {
+            if (1 == node.nodeType) {
+                startNode = node;
+            }
+            let childNodeId = node.nodeTo;
+            if (nodesGroup.hasOwnProperty(childNodeId)) {
+                let itemNode = nodesGroup[childNodeId];
+                Object.assign(node, { childNode: itemNode });
+            }
+        }
+        return startNode
+    }
+}

+ 172 - 0
src/utils/workflow/formatcommit_data.js

@@ -0,0 +1,172 @@
+// import { FormatUtils } from '@/utils/formatcommit_data'
+// import { NodeUtils } from '@/utils/nodeUtils'
+const isEmpty = data => data === null || data === undefined || data === ''
+const isEmptyArray = data => Array.isArray(data) ? data.length === 0 : true
+
+export class FormatUtils {
+    /**
+     * 对基础设置,高级设置等设置页内容进行格式化
+     * @param params
+     */
+    static formatSettings(param) {
+        let treeList = this.flattenMapTreeToList(param);
+        let combinationList = this.getEndpointNodeId(treeList);
+        let finalList = this.cleanNodeList(combinationList); 
+        let fomatList = this.adapterActivitiNodeList(finalList);
+        return fomatList; 
+    }
+    /**
+    * 展平树结构
+    * @param {Object} treeData  - 节点数据 
+    * @returns Array - 节点数组
+    */
+    static flattenMapTreeToList(treeData) {
+        let nodeData = [];
+        function traverse(node) {
+            if (node.nodeType == 2) {
+                if (node.childNode) {
+                    node.childNode.nodeFrom = node.nodeId;
+                    traverse(node.childNode);
+                }
+                if (!isEmptyArray(node.conditionNodes)) {
+                    for (const child of node.conditionNodes) {
+                        child.nodeFrom = node.nodeId;
+                        traverse(child);
+                    }
+                    node.nodeTo = node.conditionNodes.map(item => item.nodeId);
+                    delete node.conditionNodes
+                }
+            }
+            else if (node.childNode) {
+                node.nodeTo = [node.childNode.nodeId];
+                node.childNode.nodeFrom = node.nodeId;
+                traverse(node.childNode);
+            }
+            delete node.childNode
+            nodeData.push(node);
+        }
+        traverse(treeData);
+        return nodeData;
+    }
+    /**
+     * 递归处理网关节点下属子节点的nodeTo数据
+     * @param { Array } parmData -节点关系数组 
+     * @returns 
+     */
+    static getEndpointNodeId(parmData) {
+
+        if (isEmptyArray(parmData)) return parmData;
+
+        let getwayList = parmData.filter((c) => {
+            return c.nodeType == 2;
+        });
+
+        if (isEmptyArray(getwayList)) return parmData;
+
+        let nodesGroup = {};
+        for (let t of parmData) {
+            if (nodesGroup.hasOwnProperty(t.nodeFrom)) {
+                nodesGroup[t.nodeFrom].push(t)
+            } else {
+                nodesGroup[t.nodeFrom] = [t]
+            }
+        }
+        for (let getway of getwayList) {
+            if (nodesGroup.hasOwnProperty(getway.nodeId)) {
+                let itemNodes = nodesGroup[getway.nodeId];
+                let comNode = itemNodes.find((c) => { return c.nodeType != 3; });
+                if (!comNode) continue;
+                let conditionList = itemNodes.filter((c) => { return c.nodeId != comNode.nodeId; });
+                for (let itemNode of conditionList) {
+                    function internalTraverse(info) {
+                        if (info) {
+                            if (!nodesGroup[info.nodeId]) {
+                                info.nodeTo = [comNode.nodeId];
+                            } else {
+                                let tempNode = nodesGroup[info.nodeId];
+                                if (Array.isArray(tempNode)) {
+                                    for (let t_item of tempNode) {
+                                        internalTraverse(t_item);
+                                    }
+                                }
+                                else {
+                                    internalTraverse(tempNode);
+                                }
+                            }
+                        }
+                    }
+                    internalTraverse(itemNode);
+                }
+            }
+        }
+        return parmData;
+    }
+    /**
+     * 清理节点数据
+     * @param { Array } arr -节点数组
+     * @returns 
+     */
+    static cleanNodeList(arr) {
+        let nodeIds = arr.map((c) => { return c.nodeId; });
+        for (const node of arr) {
+            node.nodeTo = Array.from(new Set(node.nodeTo));
+            if (!isEmptyArray(node.nodeTo)) {
+                node.nodeTo = node.nodeTo.filter((key) => {
+                    return nodeIds.indexOf(key) > -1;
+                });
+            }
+        }
+        return arr;
+    }
+
+    /**
+     * 格式化node数据,对接api接口
+     * @param {Array} nodeList 
+     * @returns 
+     */
+    static adapterActivitiNodeList(nodeList) {
+
+        for (let node of nodeList) {
+            if (node.hasOwnProperty('id')) {
+                delete node.id;
+            }
+            if (node.nodeType == 3) {
+                let conditionObj = {
+                    conditionList: node.conditionList,
+                    sort: node.priorityLevel,
+                    isDefault: node.isDefault
+                };
+                Object.assign(node, { property: {} });
+                node.property = conditionObj;
+                delete node.conditionList;
+            }
+
+            if (node.nodeType == 4 || node.nodeType == 6) { 
+                let approveObj = {
+                    emplIds: [],
+                    emplList: [],
+                    roleIds: [],
+                    roleList: [],
+                    hrbpConfType: 0,
+                    assignLevelGrade: 0,
+                    signType: node.signType,
+                }
+                if (node.nodeApproveList && !isEmptyArray(node.nodeApproveList)) { 
+                    for (let approve of node.nodeApproveList) {
+                        let emp={};
+                        emp.id=parseInt(approve.targetId);
+                        emp.name=approve.name;
+                        approveObj.emplIds.push(parseInt(approve.targetId));
+                        approveObj.emplList.push(emp);
+                    }
+                }
+                node.nodeProperty = node.setType;
+                node.property = approveObj;
+                delete node.nodeApproveList;
+            }
+        }
+        return nodeList;
+    }
+
+}
+

+ 107 - 0
src/utils/workflow/formatdisplay_data.js

@@ -0,0 +1,107 @@
+// import { FormatDisplayUtils } from '@/utils/formatdisplay_data'
+const isEmptyArray = data => Array.isArray(data) ? data.length === 0 : true
+
+export class FormatDisplayUtils {
+    /**
+     * 格式化显示数据
+     * @param {Array} parmData 
+     * @returns Object
+     */
+    static getToTree(parmData) {
+        let node = this.createNodeDisplay(parmData);
+        let formatList = this.formatDisplayStructNodeList(parmData.nodes);
+        node.nodeConfig = this.depthConverterToTree(formatList);//parmData.nodes
+        return node
+    }
+
+    /**
+     * 创建Node Data 数据
+     * @param { Object } nodeData - 源节点数据
+     * @returns Object
+     */
+    static createNodeDisplay(nodeData) {
+        let displayObj = {
+            flowId: nodeData.id,
+            bpmnCode: nodeData.bpmnCode,
+            bpmnName: nodeData.bpmnName, //name 改成 bpmnName 其他的都是添加的
+            bpmnType: nodeData.bpmnType,
+            formCode: nodeData.formCode,
+            appId: nodeData.appId,
+            deduplicationType: nodeData.deduplicationType,//2去重,1不去重
+            effectiveStatus: nodeData.effectiveStatus == 1 ? true : false,
+            remark: nodeData.remark,
+            isDel: 0,
+            directorMaxLevel: 3,
+            nodeConfig: {},
+        }
+        return displayObj
+    }
+    /**
+     * List 转成tree结构
+     * @param {Array} parmData 
+     * @returns 
+     */
+    static depthConverterToTree(parmData) {
+        if (isEmptyArray(parmData)) return
+        let nodesGroup = {}, startNode = {}
+        for (let t of parmData) {
+            if (nodesGroup.hasOwnProperty(t.nodeFrom)) {
+                nodesGroup[t.nodeFrom].push(t)
+            } else {
+                nodesGroup[t.nodeFrom] = [t]
+            }
+        }
+        for (let node of parmData) {
+            if (1 == node.nodeType) {
+                startNode = node;
+            }
+            Object.assign(node, { conditionNodes: [] });
+            let currNodeId = node.nodeId;
+            if (nodesGroup.hasOwnProperty(currNodeId)) {
+                let itemNodes = nodesGroup[currNodeId];
+                for (let itemNode of itemNodes) {
+                    if (3 == itemNode.nodeType) {
+                        node.conditionNodes.push(itemNode);
+                    } else {
+                        node.childNode = itemNode;
+                    }
+                }
+            }
+        }
+        return startNode
+    }
+
+    static formatDisplayStructNodeList(nodeList) {
+        if (isEmptyArray(nodeList)) return nodeList;
+        for (let node of nodeList) {
+
+            if (node.nodeType == 3) {
+                node.priorityLevel = node.property.sort;
+                node.isDefault = node.property.isDefault;
+                Object.assign(node, { conditionList: [] });
+                node.conditionList = node.property.conditionList ? node.property.conditionList : [];
+                delete node.property;
+            }
+
+            if (node.nodeType == 4 || node.nodeType == 6) {
+                let empList = [];
+                if (node.property && !isEmptyArray(node.property.emplList)) {
+                    for (let emp of node.property.emplList) {
+                        let approveObj = {
+                            type: 1,
+                            targetId: parseInt(emp.id),
+                            name: emp.name
+                        };
+                        empList.push(approveObj);
+                    } 
+                    Object.assign(node, { signType: node.property.signType });
+                }
+                node.setType = node.nodeProperty;
+                Object.assign(node, { nodeApproveList: [] }); 
+                node.nodeApproveList = empList;
+                delete node.property;
+            }
+        }
+        return nodeList;
+    }
+}

+ 237 - 0
src/utils/workflow/hsharpUtils.js

@@ -0,0 +1,237 @@
+
+
+/**
+ * 通用js方法封装处理 
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+  if (this.$refs[refName]) {
+    this.$refs[refName].resetFields();
+  }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+  let search = params;
+  search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+  dateRange = Array.isArray(dateRange) ? dateRange : [];
+  if (typeof (propName) === 'undefined') {
+    search.params['beginTime'] = dateRange[0];
+    search.params['endTime'] = dateRange[1];
+  } else {
+    search.params['begin' + propName] = dateRange[0];
+    search.params['end' + propName] = dateRange[1];
+  }
+  return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+  if (value === undefined) {
+    return "";
+  }
+  var actions = [];
+  Object.keys(datas).some((key) => {
+    if (datas[key].value == ('' + value)) {
+      actions.push(datas[key].label);
+      return true;
+    }
+  })
+  if (actions.length === 0) {
+    actions.push(value);
+  }
+  return actions.join('');
+}
+
+// 回显数据字典(字符串、数组)
+export function selectDictLabels(datas, value, separator) {
+  if (value === undefined || value.length ===0) {
+    return "";
+  }
+  if (Array.isArray(value)) {
+    value = value.join(",");
+  }
+  var actions = [];
+  var currentSeparator = undefined === separator ? "," : separator;
+  var temp = value.split(currentSeparator);
+  Object.keys(value.split(currentSeparator)).some((val) => {
+    var match = false;
+    Object.keys(datas).some((key) => {
+      if (datas[key].value == ('' + temp[val])) {
+        actions.push(datas[key].label + currentSeparator);
+        match = true;
+      }
+    })
+    if (!match) {
+      actions.push(temp[val] + currentSeparator);
+    }
+  })
+  return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+  var args = arguments, flag = true, i = 1;
+  str = str.replace(/%s/g, function () {
+    var arg = args[i++];
+    if (typeof arg === 'undefined') {
+      flag = false;
+      return '';
+    }
+    return arg;
+  });
+  return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+  if (!str || str == "undefined" || str == "null") {
+    return "";
+  }
+  return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+  for (var p in target) {
+    try {
+      if (target[p].constructor == Object) {
+        source[p] = mergeRecursive(source[p], target[p]);
+      } else {
+        source[p] = target[p];
+      }
+    } catch (e) {
+      source[p] = target[p];
+    }
+  }
+  return source;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[parentId].push(d);
+  }
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (nodeIds[parentId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params  参数
+*/
+export function tansParams(params) {
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    var part = encodeURIComponent(propName) + "=";
+    if (value !== null && value !== "" && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+            let params = propName + '[' + key + ']';
+            var subPart = encodeURIComponent(params) + "=";
+            result += subPart + encodeURIComponent(value[key]) + "&";
+          }
+        }
+      } else {
+        result += part + encodeURIComponent(value) + "&";
+      }
+    }
+  }
+  return result
+}
+
+// 验证是否为blob格式
+export function blobValidate(data) {
+  return data.type !== 'application/json'
+}
+// 返回上一步
+export function goBack() {
+  window.history.go(-1);
+}
+ 

+ 176 - 0
src/utils/workflow/index.js

@@ -0,0 +1,176 @@
+function All() {}
+All.prototype = {
+    timer: "",
+    debounce(fn, delay = 500) {
+        var _this = this;
+        return function(arg) {
+            //获取函数的作用域和变量
+            let that = this;
+            let args = arg;
+            clearTimeout(_this.timer) // 清除定时器
+            _this.timer = setTimeout(function() {
+                fn.call(that, args)
+            }, delay)
+        }
+    },
+    setCookie(val) { //cookie设置[{key:value}]、获取key、清除['key1','key2']
+        for (var i = 0, len = val.length; i < len; i++) {
+            for (var key in val[i]) {
+                document.cookie = key + '=' + encodeURIComponent(val[i][key]) + "; path=/";
+            }
+        }
+    },
+    getCookie(name) {
+        var strCookie = document.cookie;
+        var arrCookie = strCookie.split("; ");
+        for (var i = 0, len = arrCookie.length; i < len; i++) {
+            var arr = arrCookie[i].split("=");
+            if (name == arr[0]) {
+                return decodeURIComponent(arr[1]);
+            }
+        }
+    },
+    clearCookie(name) {
+        var myDate = new Date();
+        myDate.setTime(-1000); //设置时间    
+        for (var i = 0, len = name.length; i < len; i++) {
+            document.cookie = "" + name[i] + "=''; path=/; expires=" + myDate.toGMTString();
+        }
+    },
+    arrToStr(arr) {
+        if (arr) {
+            return arr.map(item => { return item.name }).toString()
+        }
+    },
+    toggleClass(arr, elem, key = 'id') {
+        return arr.some(item => { return item[key] == elem[key] });
+    },
+    toChecked(arr, elem, key = 'id') {
+        var isIncludes = this.toggleClass(arr, elem, key);
+        !isIncludes ? arr.push(elem) : this.removeEle(arr, elem, key);
+    },
+    removeEle(arr, elem, key = 'id') {
+        var includesIndex;
+        arr.map((item, index) => {
+            if (item[key] == elem[key]) {
+                includesIndex = index
+            }
+        });
+        arr.splice(includesIndex, 1);
+    },
+    setApproverStr(nodeConfig) {
+        if (nodeConfig.setType == 1) {
+            if (nodeConfig.nodeApproveList.length == 1) {
+                return nodeConfig.nodeApproveList[0].name
+            } else if (nodeConfig.nodeApproveList.length > 1) {
+                if (nodeConfig.signType == 1) {
+                    return this.arrToStr(nodeConfig.nodeApproveList)
+                } else if (nodeConfig.signType == 2) {
+                    return nodeConfig.nodeApproveList.length + "人(" + this.arrToStr(nodeConfig.nodeApproveList) + ")会签"
+                }
+            }
+        } else if (nodeConfig.setType == 2) {
+            let level = nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
+            if (nodeConfig.signType == 1) {
+                return level
+            } else if (nodeConfig.signType == 2) {
+                return level + "会签"
+            }
+        }else if (nodeConfig.setType == 3) {
+            if (nodeConfig.nodeApproveList.length > 0) {
+                return  "指定 (" + this.arrToStr(nodeConfig.nodeApproveList) + ") 角色"
+            }
+            return ""
+        } else if (nodeConfig.setType == 4) {
+            return "指定部门"
+        } else if (nodeConfig.setType == 5) {
+            return "发起人自己"
+        } else if (nodeConfig.setType == 6) {
+            return "层层审批:直到发起人的第"+ nodeConfig.directorLevel +"级主管"
+        }  
+    },
+    dealStr(str, obj) {
+        let arr = [];
+        let list = str.split(",");
+        for (var elem in obj) {
+            list.map(item => {
+                if ((item -1) == elem) {
+                    arr.push(obj[elem].value)
+                }
+            })
+        }
+        return arr.join("或")
+    },  
+    getLabelStr(index, obj) {  
+        if(!obj) return; 
+        let ret = obj[index -1];
+        if (ret) {
+            return ret.value;
+        }
+        return '';
+    },  
+    conditionStr(nodeConfig, index) {
+        var { conditionList, nodeApproveList } = nodeConfig.conditionNodes[index];
+        if (conditionList.length == 0) {
+            return (index == nodeConfig.conditionNodes.length - 1) && nodeConfig.conditionNodes[index].conditionList.length == 0 ? '其他条件进入此流程' : '请设置条件'
+        } else {
+            let str = ""
+            for (var i = 0; i < conditionList.length; i++) {
+                var { formId, columnType, showType, showName, optType, zdy1, opt1, zdy2, opt2, fixedDownBoxValue } = conditionList[i];
+                if (formId == 0) {
+                    if (nodeApproveList.length != 0) {
+                        str += '发起人属于:'
+                        str += nodeApproveList.map(item => { return item.name }).join("或") + " 并且 "
+                    }
+                }
+                else if (columnType == "String" && showType == "3") {
+                    if (zdy1) {
+                        str += showName + '属于:' + this.dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + " 并且 "
+                    }
+                }
+                else if (columnType == "String" && showType == "2") {
+                    if (!fixedDownBoxValue) {
+                        str += nodeConfig.conditionNodes[index].nodeDisplayName + "     "
+                    }else {
+                        if (zdy1) {
+                            str += showName + ':' + this.getLabelStr(zdy1, JSON.parse(fixedDownBoxValue)) + " 并且 "
+                        }
+                    }                  
+                }
+                else if (columnType == "Double" && showType == "2") {
+                    if (zdy1) {
+                        str += showName + ':' + this.getLabelStr(zdy1, JSON.parse(fixedDownBoxValue)) + " 并且 "
+                    }
+                }
+                else if (columnType == "Double" && showType != "2") {
+                    if (optType != 6 && zdy1) {
+                        var optTypeStr = ["", "<", ">", "≤", "=", "≥"][optType]
+                        str += `${showName} ${optTypeStr} ${zdy1} 并且 `
+                    } else if (optType == 6 && zdy1 && zdy2) {
+                        str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
+                    }
+                }
+                else {
+                    str += null
+                }
+            }
+            return str ? str.substring(0, str.length - 4) : '请设置条件'
+        }
+    },
+    copyerStr(nodeConfig) {
+        if (nodeConfig.nodeApproveList.length != 0) {
+            return this.arrToStr(nodeConfig.nodeApproveList)
+        } else {
+            if (nodeConfig.ccFlag == 1) {
+                return "发起人自选"
+            }
+        }
+    }, 
+    toggleStrClass(item, key) {
+        let a = item.zdy1 ? item.zdy1.split(",") : []
+        return a.some(item => { return item == key });
+    },
+}
+
+export default new All();
+

+ 32 - 0
src/utils/workflow/loading.js

@@ -0,0 +1,32 @@
+// /**
+//  * 全局loading效果:合并多次loading请求,避免重复请求
+//  * 当调⽤⼀次showLoading,则次数+1;当次数为0时,则显⽰loading
+//  * 当调⽤⼀次hideLoading,则次数-1; 当次数为0时,则结束loading
+//  */
+// import { ElLoading } from "element-plus";
+// // 定义⼀个请求次数的变量,⽤来记录当前页⾯总共请求的次数
+// let loadingRequestCount = 0;
+// // 初始化loading
+// let loadingInstance;
+// // 显⽰loading的函数并且记录请求次数 ++
+// const showLoading = () => {
+//   if (loadingRequestCount === 0) {
+//     // 全局实现loading效果,不⽤每个页⾯单独去v-loading
+//     // loading样式
+//     loadingInstance = ElLoading.service({
+//       lock: true,
+//       //text: "加载中……", 
+//       background: 'rgba(0, 0, 0, 0.1)'
+//     });
+//   }
+//   loadingRequestCount++;
+// };
+// // 隐藏loading的函数,并且记录请求次数 --
+// const closeLoading = () => {
+//   if (loadingRequestCount <= 0) return;
+//   loadingRequestCount--;
+//   if (loadingRequestCount === 0) {
+//     loadingInstance.close();
+//   }
+// };
+// export { showLoading, closeLoading };

+ 648 - 0
src/utils/workflow/nodeUtils.js

@@ -0,0 +1,648 @@
+//import {  NodeUtils } from '@/utils/nodeUtils'
+
+export class NodeUtils {
+  /**
+   * 根据自增数生成64进制id
+   * @returns 64进制id字符串
+   */
+  static idGenerator() {
+    let qutient = new Date() - new Date("2024-05-01");
+    qutient += Math.ceil(Math.random() * 1000); // 防止重複
+    const chars =
+      "0123456789ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz";
+    const charArr = chars.split("");
+    const radix = chars.length;
+    const res = [];
+    do {
+      let mod = qutient % radix;
+      qutient = (qutient - mod) / radix;
+      res.push(charArr[mod]);
+    } while (qutient);
+    return res.join("").toUpperCase();
+  }
+  /**
+   * 初始化流程数据
+   * @returns object
+   */
+  static createStartNode() {
+    let startObj = {
+      data: {},
+    };
+    let startNode = {
+      bpmnName: "请假申请流程",
+      bpmnCode: "BIZ_RTWHMN",
+      bpmnType: null,
+      flowGroup: 1,
+      formCode: "LEAVE_WMA",
+      remark: "",
+      effectiveStatus: 0,
+      deduplicationType: 1,
+      isLowCodeFlow: 0, // 0、自定义表单 1、低代码表单 演示环境默认为0
+      formData: {
+        widgetList: [
+          {
+            key: 21993,
+            type: "grid",
+            category: "container",
+            icon: "grid",
+            cols: [
+              {
+                type: "grid-col",
+                category: "container",
+                icon: "grid-col",
+                internal: true,
+                widgetList: [
+                  {
+                    key: 31925,
+                    type: "input",
+                    icon: "text-field",
+                    formItemFlag: true,
+                    options: {
+                      name: "input73847",
+                      label: "姓名",
+                      labelAlign: "",
+                      type: "text",
+                      defaultValue: "",
+                      placeholder: "",
+                      columnWidth: "200px",
+                      size: "",
+                      labelWidth: null,
+                      labelHidden: false,
+                      readonly: false,
+                      disabled: false,
+                      hidden: false,
+                      clearable: true,
+                      showPassword: false,
+                      required: false,
+                      requiredHint: "",
+                      validation: "",
+                      validationHint: "",
+                      customClass: "",
+                      labelIconClass: null,
+                      labelIconPosition: "rear",
+                      labelTooltip: null,
+                      minLength: null,
+                      maxLength: null,
+                      showWordLimit: false,
+                      prefixIcon: "",
+                      suffixIcon: "",
+                      appendButton: false,
+                      appendButtonDisabled: false,
+                      buttonIcon: "custom-search",
+                      onCreated: "",
+                      onMounted: "",
+                      onInput: "",
+                      onChange: "",
+                      onFocus: "",
+                      onBlur: "",
+                      onValidate: "",
+                      onAppendButtonClick: "",
+                      fieldTypeName: "input",
+                      fieldType: "1",
+                    },
+                    id: "input73847",
+                  },
+                ],
+                options: {
+                  name: "gridCol96743",
+                  hidden: false,
+                  span: 12,
+                  offset: 0,
+                  push: 0,
+                  pull: 0,
+                  responsive: false,
+                  md: 12,
+                  sm: 12,
+                  xs: 12,
+                  customClass: "",
+                },
+                id: "grid-col-96743",
+              },
+              {
+                type: "grid-col",
+                category: "container",
+                icon: "grid-col",
+                internal: true,
+                widgetList: [
+                  {
+                    key: 31925,
+                    type: "input",
+                    icon: "text-field",
+                    formItemFlag: true,
+                    options: {
+                      name: "input51574",
+                      label: "年龄",
+                      labelAlign: "",
+                      type: "text",
+                      defaultValue: "",
+                      placeholder: "",
+                      columnWidth: "200px",
+                      size: "",
+                      labelWidth: null,
+                      labelHidden: false,
+                      readonly: false,
+                      disabled: false,
+                      hidden: false,
+                      clearable: true,
+                      showPassword: false,
+                      required: false,
+                      requiredHint: "",
+                      validation: "",
+                      validationHint: "",
+                      customClass: "",
+                      labelIconClass: null,
+                      labelIconPosition: "rear",
+                      labelTooltip: null,
+                      minLength: null,
+                      maxLength: null,
+                      showWordLimit: false,
+                      prefixIcon: "",
+                      suffixIcon: "",
+                      appendButton: false,
+                      appendButtonDisabled: false,
+                      buttonIcon: "custom-search",
+                      onCreated: "",
+                      onMounted: "",
+                      onInput: "",
+                      onChange: "",
+                      onFocus: "",
+                      onBlur: "",
+                      onValidate: "",
+                      onAppendButtonClick: "",
+                      fieldTypeName: "input",
+                      fieldType: "1",
+                    },
+                    id: "input51574",
+                  },
+                ],
+                options: {
+                  name: "gridCol29220",
+                  hidden: false,
+                  span: 12,
+                  offset: 0,
+                  push: 0,
+                  pull: 0,
+                  responsive: false,
+                  md: 12,
+                  sm: 12,
+                  xs: 12,
+                  customClass: "",
+                },
+                id: "grid-col-29220",
+              },
+            ],
+            options: {
+              name: "grid48769",
+              hidden: false,
+              gutter: 12,
+              colHeight: null,
+              customClass: "",
+            },
+            id: "grid48769",
+          },
+          {
+            key: 92385,
+            type: "textarea",
+            icon: "textarea-field",
+            formItemFlag: true,
+            options: {
+              name: "textarea81541",
+              label: "备注",
+              labelAlign: "",
+              rows: 3,
+              defaultValue: "",
+              placeholder: "",
+              columnWidth: "200px",
+              size: "",
+              labelWidth: null,
+              labelHidden: false,
+              readonly: false,
+              disabled: false,
+              hidden: false,
+              required: false,
+              requiredHint: "",
+              validation: "",
+              validationHint: "",
+              customClass: "",
+              labelIconClass: null,
+              labelIconPosition: "rear",
+              labelTooltip: null,
+              minLength: null,
+              maxLength: null,
+              showWordLimit: false,
+              onCreated: "",
+              onMounted: "",
+              onInput: "",
+              onChange: "",
+              onFocus: "",
+              onBlur: "",
+              onValidate: "",
+              fieldTypeName: "textarea",
+              fieldType: "1",
+            },
+            id: "textarea81541",
+          },
+        ],
+        formConfig: {
+          modelName: "formData",
+          refName: "vForm",
+          rulesName: "rules",
+          labelWidth: 80,
+          labelPosition: "left",
+          size: "",
+          labelAlign: "label-left-align",
+          cssCode: "",
+          customClass: [],
+          functions: "",
+          layoutType: "PC",
+          jsonVersion: 3,
+          onFormCreated: "",
+          onFormMounted: "",
+          onFormDataChange: "",
+          onFormValidate: "",
+        },
+      },
+      nodes: [
+        {
+          nodeId: "S2MHMN",
+          nodeName: "审核人",
+          nodeType: 4,
+          nodeFrom: "4FIHMN",
+          nodeTo: [],
+          setType: 1,
+          directorLevel: 1,
+          signType: 1,
+          noHeaderAction: 1,
+          error: false,
+          property: {
+            emplIds: [1001],
+            emplList: [{ id: 1001, name: "管理员" }],
+            roleIds: [],
+            roleList: [],
+            hrbpConfType: 0,
+            assignLevelGrade: 0,
+            signType: 1,
+          },
+          buttons: { startPage: [1], approvalPage: [3, 4], viewPage: [0] },
+          nodeProperty: 1,
+        },
+        {
+          nodeId: "ABHHMN",
+          nodeName: "审核人",
+          nodeType: 4,
+          nodeFrom: "L5IHMN",
+          nodeTo: ["S2MHMN"],
+          setType: 1,
+          directorLevel: 1,
+          signType: 1,
+          noHeaderAction: 1,
+          error: false,
+          property: {
+            emplIds: [1],
+            emplList: [{ id: 1, name: "张三" }],
+            roleIds: [],
+            roleList: [],
+            hrbpConfType: 0,
+            assignLevelGrade: 0,
+            signType: 1,
+          },
+          buttons: { startPage: [1], approvalPage: [3, 4], viewPage: [0] },
+          nodeProperty: 1,
+        },
+        {
+          nodeId: "L5IHMN",
+          nodeName: "条件1",
+          nodeDisplayName: "请假时长 ≥ 32",
+          nodeType: 3,
+          nodeFrom: "4FIHMN",
+          nodeTo: ["ABHHMN"],
+          priorityLevel: 1,
+          nodeApproveList: [],
+          error: false,
+          isDefault: 0,
+          property: {
+            conditionList: [
+              {
+                formId: "2",
+                columnId: "2",
+                showType: "1",
+                type: 2,
+                showName: "请假时长",
+                optType: "5",
+                zdy1: "32",
+                opt1: "<",
+                zdy2: "",
+                opt2: "<",
+                columnDbname: "leaveHour",
+                fieldTypeName: "input-number",
+                columnType: "Double",
+                fixedDownBoxValue: "",
+              },
+            ],
+            sort: 1,
+            isDefault: 0,
+          },
+        },
+        {
+          nodeId: "38IHMN",
+          nodeName: "条件2",
+          nodeDisplayName: "其他条件进入此流程",
+          nodeType: 3,
+          nodeFrom: "4FIHMN",
+          nodeTo: ["S2MHMN"],
+          priorityLevel: 2,
+          nodeApproveList: [],
+          error: false,
+          isDefault: 1,
+          property: { conditionList: [], sort: 2, isDefault: 1 },
+        },
+        {
+          nodeId: "4FIHMN",
+          nodeName: "网关",
+          nodeType: 2,
+          nodeFrom: "Gb2",
+          nodeTo: ["L5IHMN", "38IHMN"],
+          error: true,
+          property: null,
+        },
+        {
+          confId: 35,
+          nodeId: "Gb2",
+          nodeType: 1,
+          nodeProperty: 1,
+          nodePropertyName: null,
+          nodeFrom: "",
+          nodeFroms: null,
+          prevId: [],
+          batchStatus: 0,
+          approvalStandard: 2,
+          nodeName: "发起人",
+          nodeDisplayName: "发起人",
+          annotation: null,
+          isDeduplication: 0,
+          isSignUp: 0,
+          orderedNodeType: null,
+          remark: "",
+          isDel: 0,
+          nodeTo: ["4FIHMN"],
+          property: null,
+          params: null,
+          buttons: { startPage: [], approvalPage: [2], viewPage: null },
+          templateVos: null,
+          approveRemindVo: null,
+          conditionNodes: [],
+        },
+      ],
+    };
+    startObj.data = startNode;
+    return startObj;
+  }
+  /**
+   * 创建审批人对象
+   */
+  static createApproveNode(child) {
+    let approveNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "审核人",
+      nodeDisplayName: "审核人",
+      nodeType: 4,
+      nodeFrom: "",
+      nodeTo: [],
+      setType: 1,
+      directorLevel: 1,
+      signType: 1,
+      noHeaderAction: 1,
+      childNode: child,
+      error: true,
+      property: {
+        afterSignUpWay: 1,
+      },
+      buttons: {
+        startPage: [1],
+        approvalPage: [3, 4],
+        viewPage: [0],
+      },
+      nodeApproveList: [],
+    };
+    return approveNode;
+  }
+  /**
+   * 创建抄送人对象
+   * @returns object
+   */
+  static createCopyNode(child) {
+    let copyNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "抄送人",
+      nodeDisplayName: "抄送人",
+      nodeType: 6,
+      nodeFrom: "",
+      nodeTo: [],
+      setType: 1,
+      error: true,
+      ccFlag: 1,
+      childNode: child,
+      property: {},
+      buttons: {
+        startPage: [],
+        approvalPage: [],
+        viewPage: [],
+      },
+      nodeApproveList: [],
+    };
+    return copyNode;
+  } 
+  /**
+   * 创建网关对象
+   * @returns object
+   */
+  static createGatewayNode(child) {
+    let gatewayNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "网关",
+      nodeType: 2,
+      nodeFrom: "",
+      nodeTo: [],
+      childNode: null,
+      isDynamicCondition: false, //true 动态条件 false 非动态条件
+      isParallel: false, //true 是并行条件 false 非并行条件
+      error: true,
+      property: null,
+      conditionNodes: [
+        this.createConditionNode("条件1", child, 1, 0),
+        this.createConditionNode("条件2", null, 2, 1),
+      ],
+    };
+    return gatewayNode;
+  }
+  /**
+   * 创建动态网关对象
+   * @returns object
+   */
+  static createDynamicConditionWayNode(child) {
+    let gatewayNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "动态网关",
+      nodeType: 2,
+      nodeFrom: "",
+      nodeTo: [],
+      childNode: null,
+      isDynamicCondition: true, //true 动态条件 false 非动态条件
+      isParallel: false, //true 是并行条件 false 非并行条件
+      error: false,
+      property: null,
+      conditionNodes: [
+        this.createConditionNode("动态条件1", child, 1, 0),
+        this.createConditionNode("动态条件2", null, 2, 1),
+      ],
+    };
+    return gatewayNode;
+  }
+  /**
+   * 创建条件并行网关对象
+   * @returns object
+   */
+  static createParallelConditionWayNode(child) {
+    let gatewayNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "条件并行网关",
+      nodeType: 2,
+      nodeFrom: "",
+      nodeTo: [],
+      childNode: this.createParallelNode("条件并行聚合审批人", null, 1, 0),
+      isDynamicCondition: false, //true 动态条件 false 非动态条件
+      isParallel: true, //true 是并行条件 false 非并行条件
+      error: false,
+      property: null,
+      conditionNodes: [
+        this.createConditionNode("并行条件1", child, 1, 0),
+        this.createConditionNode("并行条件2", null, 2, 0),
+      ],
+    };
+    return gatewayNode;
+  }
+  /**
+   * 创建条件对象
+   * @returns object
+   */
+  static createConditionNode(name, childNode, priority, isDefault) {
+    let conditionNode = {
+      nodeId: this.idGenerator(),
+      nodeName: name || "条件1",
+      nodeDisplayName: name || "条件1",
+      nodeType: 3,
+      nodeFrom: "",
+      nodeTo: [],
+      priorityLevel: priority,
+      conditionList: [],
+      nodeApproveList: [],
+      error: true,
+      childNode: childNode,
+      isDefault: isDefault || 0,
+    };
+    return conditionNode;
+  }
+
+  /**
+   * 条件判断对象
+   * @param {*} formId  条件表单Id
+   * @param {*} columnId 条件判断id
+   * @param {*} type 类型 1,发起人 2,其他表单条件
+   * @param {*} showName 显示名称.
+   * @param {*} showType //1,值类型(>,>=,<,<=,=),2单选下拉, 3多选(checkbox) 其他
+   * @param {*} columnName  DB字段名称
+   * @param {*} columnType  DB字段类型
+   * @param {*} fixedDownBoxValue 条件选项
+   * @returns
+   */
+  static createJudgeNode(
+    formId,
+    type,
+    showName,
+    showType,
+    columnName,
+    columnType,
+    fixedDownBoxValue
+  ) {
+    let judgeNode = {
+      formId: formId,
+      columnId: formId,
+      showType: showType,
+      type: type, //1,发起人 2,其他表单条件
+      showName: showName,
+      optType: "5",
+      zdy1: "",
+      opt1: "<",
+      zdy2: "",
+      opt2: "<",
+      columnDbname: columnName,
+      columnType: columnType,
+      fixedDownBoxValue: fixedDownBoxValue,
+    };
+    return judgeNode;
+  }
+
+  /**
+   * 创建并行网关对象
+   * @returns object
+   */
+  static createParallelWayNode(child) {
+    let parallelwayNode = {
+      nodeId: this.idGenerator(),
+      nodeName: "并行审核网关",
+      nodeType: 7,
+      nodeFrom: "",
+      nodeTo: [],
+      childNode: this.createParallelNode("并行聚合节点", null, 1, 0),
+      error: false,
+      property: null,
+      parallelNodes: [
+        this.createParallelNode("并行审核人1", child, 1, 0),
+        this.createParallelNode("并行审核人2", null, 2, 0),
+      ],
+    };
+    return parallelwayNode;
+  }
+  /**
+   * 创建并行审批人对象
+   * @returns object
+   */
+  static createParallelNode(name, childNode, priority, isDefault) {
+    let parallelNode = {
+      nodeId: this.idGenerator(),
+      nodeName: name || "并行审核人1",
+      nodeDisplayName: "",
+      nodeType: 4, //节点类型 4、审批人
+      nodeFrom: "",
+      nodeTo: [],
+      priorityLevel: priority,
+      nodeApproveList: [],
+      setType: 1, //审批人类型 1、指定人员
+      signType: 1, //审批方式 1:会签-需全部同意,2:或签-一人同意即可,3:顺序会签
+      isSignUp: 0, //是否加批 0:否,1:是
+      noHeaderAction: 1,
+      lfFieldControlVOs: [],
+      property: {
+        afterSignUpWay: 2, //是否回到加批人 1:是,2:否
+        signUpType: 1, //加批类型 1:顺序会签,2:会签 特别 3指: 回到加批人,afterSignUpWay赋值为1,signUpType赋值为1
+      },
+      buttons: {
+        startPage: [1],
+        approvalPage: [3, 4, 18, 21],
+        viewPage: [0],
+      },
+      error: true,
+      childNode: childNode,
+      isDefault: isDefault || 0,
+    };
+    return parallelNode;
+  }
+}
+
+/**
+ * 添模拟数据
+ */
+export function getMockData() {
+  let startNode = ""; //NodeUtils.createNode("start", "");
+  return startNode;
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels