Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

liuc 7 месяцев назад
Родитель
Сommit
a506fca370

+ 38 - 4
src/components/BsUi/Descriptions/index.vue

@@ -12,19 +12,31 @@
       <div class="default_slot" v-if="slots.default">
         <slot></slot>
       </div>
-      <a-descriptions v-if="!isEmpty(items)" :bordered="false">
+      <a-descriptions v-if="!isEmpty(items)" :bordered="false" v-bind="extraProps">
         <a-descriptions-item v-for="(item, index) in items" :key="index" v-bind="item.extraProps">
           <template #label>
             <slot v-if="item.labelSlot" :name="item.labelSlot"></slot>
-            <span v-else class="dsc-label">{{ item.label }}</span>
+
+            <div v-else class="help-config">
+              <a-tooltip placement="top" v-if="item?.helpConfig?.enable">
+                <template #title>
+                  <span>{{ item?.helpConfig.helpTip }}</span>
+                </template>
+                <QuestionCircleOutlined />
+              </a-tooltip>
+              <span class="dsc-label">{{ item.label }}</span>
+            </div>
+
           </template>
 
           <template v-if="item.valueSlot">
-            <slot :name="item.valueSlot"></slot>
+           <div class="value_slot">
+             <slot :name="item.valueSlot"></slot>
+           </div>
           </template>
           <template v-if="!item.valueSlot">
             <div class="dsc-value">
-              {{ item.value }}
+              <bs-ellipsis-text :text="item.value" />
             </div>
           </template>
         </a-descriptions-item>
@@ -35,6 +47,7 @@
 <script setup>
   import { isEmpty } from 'lodash';
   import {ref, useSlots} from 'vue';
+  import {BsEllipsisText} from "/@/components/BsUi/index.js";
 
   const props = defineProps({
     title: {
@@ -49,6 +62,10 @@
       required: false,
       default: false,
     },
+    extraProps: {
+      required: false,
+      default: {},
+    }
   });
 
   const foldState = ref(true);
@@ -95,10 +112,20 @@
       color: #6c6c6c;
     }
     .dsc-value {
+      width: 100%;
       font-size: 14px;
       color: #000;
       font-weight: 500;
     }
+    .help-config {
+      display: flex;
+      gap: 5px;
+      align-items: center;
+    }
+    :deep(.ant-descriptions-item-container) {
+      display: flex;
+      align-items: flex-start;
+    }
     :deep(.ant-descriptions-header) {
       margin: 0 0 10px 0;
     }
@@ -106,8 +133,15 @@
       margin: 0;
       padding: 10px 0 0 0;
     }
+    :deep(.ant-descriptions-item-content) {
+      position: relative;
+    }
     .default_slot {
       padding: 10px 0;
     }
+    .value_slot {
+      position: absolute;
+      top: -5px;
+    }
   }
 </style>

+ 68 - 0
src/components/BsUi/EllipsisText/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <span ref="textRef" class="ellipsis-container">
+    <span class="ellipsis-text" :style="{ lineClamp: lines }">{{ text }}</span>
+    <a-tooltip v-if="isEllipsis" :title="text">
+      <span class="ellipsis-trigger">{{ text }}</span>
+    </a-tooltip>
+  </span>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick, watch } from 'vue';
+
+const props = defineProps({
+  text: {
+    type: String,
+    required: true
+  },
+  lines: {
+    type: [Number, String],
+    default: 1
+  }
+});
+
+const textRef = ref(null);
+const isEllipsis = ref(false);
+
+const checkEllipsis = () => {
+  if (!textRef.value) return;
+
+  const textElement = textRef.value.querySelector('.ellipsis-text');
+
+  isEllipsis.value = textElement.scrollHeight > textElement.clientHeight;
+};
+
+onMounted(() => {
+  nextTick(checkEllipsis);
+});
+
+watch(() => props.text, () => {
+  nextTick(checkEllipsis);
+});
+</script>
+
+<style scoped>
+.ellipsis-container {
+  position: relative;
+  display: inline-block;
+  max-width: 100%;
+}
+
+.ellipsis-text {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 100%;
+}
+
+.ellipsis-trigger {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  opacity: 0;
+  cursor: pointer;
+}
+</style>

+ 29 - 20
src/components/BsUi/Table/Table.vue

@@ -1,5 +1,5 @@
 <template>
-  <vxe-grid class="wrapper" v-bind="gridOptions" ref="gridRef" v-fullscreen :style="{height: isFixed ? `calc(100vh - 101px)` : '100%'}">
+  <vxe-grid class="wrapper" v-bind="gridOptions" ref="gridRef" v-fullscreen :style="{ height: isFixed ? `calc(100vh - 101px)` : '100%' }">
     <template #form>
       <Search
         v-if="searchConfig && searchConfig.enable && searchConfig?.fields && searchConfig?.data"
@@ -20,9 +20,7 @@
       <div class="top-main">
         <div class="top-top" v-if="$slots.toolbarTop">
           <a-space v-if="toolbarTopConfig && toolbarTopConfig.enable" style="margin-right: 8px">
-            <a-button v-for="(btn, idx) in toolbarTopConfig.buttons" :key="btn.code" size="middle" v-bind="btn.props">{{
-              btn.title
-            }}</a-button>
+            <a-button v-for="(btn, idx) in toolbarTopConfig.buttons" :key="btn.code" size="middle" v-bind="btn.props">{{ btn.title }}</a-button>
           </a-space>
 
           <slot name="toolbarTop"></slot>
@@ -31,15 +29,16 @@
           <div class="top-left">
             <Toolbar :toolbarConfig="toolbarConfig">
               <a-space v-if="toolbarConfig.leftButtons" style="margin-right: 8px">
-                <a-button v-for="(btn, idx) in toolbarConfig.leftButtons" :key="btn.code" size="middle" v-bind="btn.props">{{
-                  btn.title
-                }}</a-button>
+                <a-button v-for="(btn, idx) in toolbarConfig.leftButtons" :key="btn.code" size="middle" v-bind="btn.props">{{ btn.title }}</a-button>
               </a-space>
               <slot name="toolbarLeft"></slot>
             </Toolbar>
           </div>
           <div class="top-right" v-if="toolbarConfig && toolbarConfig.enable">
-            <div class="top-right-left" v-if="!has(toolbarConfig, 'displayToolbar') || get(toolbarConfig, 'displayToolbar') === DISPLAY_STATE.VISIBLE">
+            <div
+              class="top-right-left"
+              v-if="!has(toolbarConfig, 'displayToolbar') || get(toolbarConfig, 'displayToolbar') === DISPLAY_STATE.VISIBLE"
+            >
               <a-tooltip placement="top">
                 <template #title>
                   <span>刷新</span>
@@ -78,10 +77,8 @@
             </div>
 
             <div class="top-right-right" v-if="toolbarConfig.rightButtons">
-              <a-space  style="margin-right: 8px">
-                <a-button v-for="(btn, idx) in toolbarConfig.rightButtons" :key="btn.code" size="middle" v-bind="btn.props">{{
-                    btn.title
-                  }}</a-button>
+              <a-space style="margin-right: 8px">
+                <a-button v-for="(btn, idx) in toolbarConfig.rightButtons" :key="btn.code" size="middle" v-bind="btn.props">{{ btn.title }}</a-button>
               </a-space>
             </div>
           </div>
@@ -90,7 +87,7 @@
     </template>
 
     <template #pager>
-      <div :class="`pager ${(!has(pagerConfig, 'isFixed') || pagerConfig.isFixed) ? 'page_fixed' : '' }`" v-if="pagerConfig && pagerConfig.enable">
+      <div :class="`pager ${!has(pagerConfig, 'isFixed') || pagerConfig.isFixed ? 'page_fixed' : ''}`" v-if="pagerConfig && pagerConfig.enable">
         <Pagination :pagerConfig="pagerConfig" />
       </div>
     </template>
@@ -113,16 +110,16 @@
 </template>
 
 <script setup>
-  import { nextTick, onMounted, ref, useSlots } from 'vue';
+  import { nextTick, onMounted, ref, useSlots, watch } from 'vue';
   import Search from './component/search/index.vue';
   import Pagination from './component/pagination/index.vue';
   import Toolbar from './component/toolbar/index.vue';
   import IndexData from './component/indexData/index.vue';
-  import { mapValues, has, isString, get } from 'lodash';
-  import {DISPLAY_STATE} from "/@/components/BsUi/constant.js";
-  const props = defineProps(['gridOptions', 'searchConfig', 'pagerConfig', 'toolbarConfig', 'getGridRef', 'mounted', 'toolbarTopConfig']);
+  import { mapValues, has, isString, get, isEmpty } from 'lodash';
+  import { DISPLAY_STATE } from '/@/components/BsUi/constant.js';
+  const props = defineProps(['gridOptions', 'searchConfig', 'pagerConfig', 'toolbarConfig', 'getGridRef', 'mounted', 'toolbarTopConfig', 'url']);
 
-  const isFixed = get(props.pagerConfig,'isFixed', true)
+  const isFixed = get(props.pagerConfig, 'isFixed', true);
 
   const $slots = useSlots();
 
@@ -160,8 +157,10 @@
   onMounted(() => {
     setSlotsCols();
     nextTick(() => {
-      props.getGridRef && props.getGridRef(gridRef.value);
-      props.mounted && props.mounted(gridRef.value);
+      if (!props.url) {
+        props.getGridRef && props.getGridRef(gridRef.value);
+        props.mounted && props.mounted(gridRef.value);
+      }
     });
   });
 
@@ -171,6 +170,16 @@
     });
   };
 
+  watch(
+    () => props.gridOptions.data,
+    (value) => {
+      if (props.url && !isEmpty(value)) {
+        props.getGridRef && props.getGridRef(gridRef.value);
+        props.mounted && props.mounted(gridRef.value);
+      }
+    }
+  );
+
   defineExpose({ gridRef });
 </script>
 

+ 1 - 0
src/components/BsUi/Tabs/index.vue

@@ -37,6 +37,7 @@ watch(
     activeKey,
     (val) => {
       emits('change', val);
+      emits('update:tabActiveKey', val);
     },
     { immediate: true }
 );

+ 5 - 2
src/components/BsUi/index.js

@@ -10,6 +10,7 @@ import BsModalTableSelector from './ModalTableSelector/index.vue';
 import BsDescriptions from "./Descriptions/index.vue"
 import BsContentsWrapper from "./ContentsWrapper/index.vue"
 import BsTabs from "./Tabs/index.vue"
+import BsEllipsisText from "./EllipsisText/index.vue"
 
 const BsUi = {
   install(app) {
@@ -22,7 +23,8 @@ const BsUi = {
     app.component('BsModalTableSelector', BsModalTableSelector);
     app.component('BsDescriptions', BsDescriptions)
     app.component('BsContentsWrapper', BsContentsWrapper)
-    app.component('BsTabs', BsTabs)
+    app.component('BsTabs', BsTabs);
+    app.component('BsEllipsisText', BsEllipsisText);
   },
 };
 
@@ -44,5 +46,6 @@ export {
   useBsForm,
   BsDescriptions,
   BsContentsWrapper,
-  BsTabs
+  BsTabs,
+  BsEllipsisText
 };

+ 107 - 17
src/components/business/page-detail-layout/index.vue

@@ -1,20 +1,32 @@
 <template>
   <div class="page-detail-layout">
     <div class="header-cont">
-      占位
       <div class="header-top">
         <div class="header-t">
-          <div class="header-t-left"></div>
-          <div class="header-t-right"></div>
+          <div class="header-t-left">
+            <span class="htf-title">{{ title }}</span>
+            <slot name="titleRight"></slot>
+          </div>
+          <div class="header-t-right" v-if="slots.toolBtn">
+            <slot name="toolBtn"></slot>
+          </div>
         </div>
+        <div class="header-index" v-if="slots.titleBottom">
+          <slot name="titleBottom"></slot>
+        </div>
+      </div>
+      <div class="header-bottom" v-if="indexConfig.sourceData.length > 0">
+        <div class="hb-item" v-for="(item, index) in indexConfig.sourceData" :key="index">
+          <span class="hb-item-top">{{ item[indexConfig.labelKey] }}</span>
+          <span class="hb-item-bottom">{{ item[indexConfig.valueKey] }}</span>
+        </div>
+      </div>
 
-        <div class="header-index"></div>
+      <div class="header-flow" v-if="!isEmpty(stepsData)">
+        <steps :doing-node-index="doingNodeIndex" @change-doing-node-index="handleChangeDoingNodeIndex" :steps="stepsData"></steps>
       </div>
-      <div class="header-bottom"></div>
     </div>
 
-    <div class="header-flow">占位</div>
-
     <div class="header-tabs">
       <bs-tabs :tabs="tabs" :tab-active-key="tabActiveKey" @change="handleChangeTabActiveKey">
         <template v-for="tab in tabs" #[tab.slotName]>
@@ -27,7 +39,15 @@
 
 <script setup>
   import { BsTabs } from '/@/components/BsUi/index.js';
+  import { useSlots } from 'vue';
+  import steps from './steps/index.vue';
+  import { isEmpty } from 'lodash';
+  const slots = useSlots();
   const props = defineProps({
+    title: {
+      required: false,
+      default: '标题',
+    },
     sceneType: {
       required: false,
       default: '',
@@ -40,14 +60,33 @@
       required: false,
       default: '',
     },
+    indexConfig: {
+      required: true,
+      default: {
+        sourceData: [],
+        labelKey: 'label',
+        valueKey: 'value',
+      },
+    },
+    doingNodeIndex: {
+      required: false,
+      default: 0,
+    },
+    stepsData: {
+      required: false,
+      default: [],
+    },
   });
 
-  const emits = defineEmits(['update:tabActiveKey'])
+  const emits = defineEmits(['update:tabActiveKey', 'update:doingNodeIndex']);
 
-  const handleChangeTabActiveKey =  (val) => {
+  const handleChangeTabActiveKey = (val) => {
     emits('update:tabActiveKey', val);
-  }
+  };
 
+  const handleChangeDoingNodeIndex = (val) => {
+    emits('update:doingNodeIndex', val);
+  };
 </script>
 
 <style lang="scss" scoped>
@@ -58,27 +97,78 @@
       width: 100%;
       border-radius: 8px;
       background: #fff;
+      padding: 20px;
       .header-top {
         width: 100%;
         .header-t {
           width: 100%;
+          display: flex;
+          justify-content: space-between;
+          .header-t-left {
+            width: 100%;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            .htf-title {
+              font-size: 18px;
+              color: #000;
+              font-weight: 600;
+            }
+          }
+
+          .header-t-right {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+          }
         }
         .header-index {
+          margin-top: 10px;
           width: 100%;
+          display: flex;
+          gap: 10px;
+          .h-i-tag-name {
+            color: #999;
+            font-size: 14px;
+            font-weight: 500;
+          }
         }
       }
 
       .header-bottom {
+        margin-top: 20px;
         width: 100%;
+        padding: 10px;
+        display: flex;
+        background: #f0f4fe;
+        border-radius: 8px;
+        gap: 20px;
+        .hb-item {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          gap: 10px;
+          min-width: 100px;
+          .hb-item-top {
+            font-size: 14px;
+            color: #999;
+            font-weight: 500;
+          }
+
+          .hb-item-bottom {
+            font-size: 14px;
+            color: #333;
+            font-weight: 500;
+          }
+        }
       }
-    }
 
-    .header-flow {
-      margin-top: 10px;
-      width: 100%;
-      height: 100%;
-      border-radius: 8px;
-      background: #fff;
+      .header-flow {
+        margin-top: 20px;
+        width: 100%;
+        height: 100%;
+        overflow-x: auto;
+      }
     }
 
     .header-tabs {

+ 170 - 0
src/components/business/page-detail-layout/steps/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="steps-progress-container">
+    <div v-for="(step, index) in steps" :key="index" class="step-item">
+      <div class="step-item-part" @click="handleClickNode(step)">
+        <!-- 步骤圆圈及勾选/数字状态 -->
+        <div
+          :class="[
+            'step-circle',
+            {
+              completed: step.status === 'completed',
+              current: step.status === 'current',
+            },
+          ]"
+        >
+          <span v-if="step.status === 'completed'">✔</span>
+          <span v-else-if="step.status === 'current'">{{ step.number }}</span>
+          <span v-else>{{ step.number }}</span>
+        </div>
+        <!-- 步骤文字及时间 -->
+        <div class="step-text">
+          <div :class="`step-title ${step.status === 'completed' ? 'step-title_completed' : ''}`">{{ step.title }}</div>
+        </div>
+
+        <div class="step-time" v-if="step.time">{{ step.time }}</div>
+      </div>
+
+      <!-- 连接线条,最后一个步骤不需要线条 -->
+      <div
+        class="step-line"
+        v-if="index < steps.length - 1"
+        :style="{
+          width: '100%',
+          background: step.status === 'completed' ? lineColor_complete : lineColor_un_complete,
+        }"
+      ></div>
+
+      <div class="line-top" v-if="index < steps.length - 1">{{ step.lineTopNum }}天</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { onMounted, ref, watch } from 'vue';
+
+  const props = defineProps({
+    doingNodeIndex: {
+      required: false,
+      default: 0,
+    },
+    steps: {
+      required: false,
+      default: [],
+    },
+  });
+
+  const doingIndex = ref(0);
+
+  // 可根据需求动态调整线条宽度和颜色等样式
+  const lineColor_complete = '#1677ff';
+  const lineColor_un_complete = '#e5e6eb';
+
+  const emits = defineEmits(['clickNode', 'changeDoingNodeIndex']);
+
+  const handleClickNode = (currentNode) => {
+    emits('clickNode', { node: currentNode });
+  };
+
+  onMounted(() => {
+    doingIndex.value = props.doingNodeIndex;
+    //   TODO 动态来处理steps中的complete和current,todo的状态
+  });
+
+  watch(doingIndex, (val) => {
+    emits('changeDoingNodeIndex', val);
+  });
+</script>
+
+<style scoped>
+  .steps-progress-container {
+    min-width: 1250px;
+    display: flex;
+    align-items: flex-start;
+    padding: 10px 0;
+    justify-content: center;
+  }
+
+  .step-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    position: relative;
+    flex: 1;
+    //width: 150px;
+  }
+
+  .step-circle {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px solid #ccc;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 14px;
+    color: #fff;
+    background-color: #ccc;
+  }
+
+  .step-circle.completed {
+    background-color: #fff;
+    border-color: var(--vxe-ui-font-primary-color);
+    color: var(--vxe-ui-font-primary-color);
+  }
+
+  .step-circle.current {
+    background-color: var(--vxe-ui-font-primary-color);
+    border-color: var(--vxe-ui-font-primary-color);
+  }
+
+  .step-text {
+    text-align: center;
+    margin-top: 5px;
+    font-size: 16px;
+    color: #86909c;
+    position: relative;
+  }
+
+  .step-time {
+    margin-top: 2px;
+    font-size: 14px;
+    color: #86909c;
+    position: absolute;
+    bottom: 0;
+    padding-left: 40px;
+  }
+
+  .step-line {
+    width: 100%;
+    height: 2px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin-left: 50px;
+    color: var(--vxe-ui-font-primary-color);
+  }
+
+  .step-item-part {
+    display: flex;
+    z-index: 100;
+    background: #fff;
+    gap: 10px;
+    padding: 20px;
+    align-items: center;
+    cursor: pointer;
+    position: relative;
+  }
+
+  .line-top {
+    position: absolute;
+    right: 0;
+    top: 10px;
+    transform: translateX(50%);
+    z-index: 100;
+    color: var(--vxe-ui-font-primary-color);
+  }
+
+  .step-title_completed {
+    color: #1d2129;
+  }
+</style>