瀏覽代碼

列表页搜索

CzRger 9 月之前
父節點
當前提交
d0499463ce

+ 0 - 9
src/api/modules/mock/global.ts

@@ -1,9 +0,0 @@
-import { handle } from '../../index'
-
-const suffix = 'mock-api'
-
-// 模拟接口
-export const mockGetUserInfo = () => handle({
-  url: `/${suffix}/getUserInfo`,
-  method: 'get',
-})

+ 17 - 0
src/api/modules/mock/mock.ts

@@ -0,0 +1,17 @@
+import { handle } from '../../index'
+
+const suffix = 'mock-api'
+
+// 模拟接口
+export const mockGetUserInfo = () => handle({
+  url: `/${suffix}/getUserInfo`,
+  method: 'get',
+})
+export const mockGetSearchHistory = () => handle({
+  url: `/${suffix}/getSearchHistory`,
+  method: 'get',
+})
+export const mockGetSearchArea = () => handle({
+  url: `/${suffix}/getSearchArea`,
+  method: 'get',
+})

二進制
src/assets/images/web/web-list_avatar.png


文件差異過大導致無法顯示
+ 8 - 0
src/assets/svg/arrow_1.svg


+ 5 - 0
src/assets/svg/arrow_2.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="17" viewBox="0 0 16 17" xmlns="http://www.w3.org/2000/svg">
+<g id="icon/&#231;&#174;&#173;&#229;&#164;&#180;">
+<path id="&#232;&#183;&#175;&#229;&#190;&#132;" d="M4 12.729C4 13.5531 4.94076 14.0234 5.6 13.529L10.9333 9.52901C11.4667 9.12901 11.4667 8.32901 10.9333 7.92901L5.6 3.92901C4.94076 3.43458 4 3.90496 4 4.72901L4 12.729Z"/>
+</g>
+</svg>

文件差異過大導致無法顯示
+ 10 - 0
src/assets/svg/close_1.svg


+ 22 - 0
src/assets/svg/flag_1.svg

@@ -0,0 +1,22 @@
+<svg width="9" height="16" viewBox="0 0 9 16" xmlns="http://www.w3.org/2000/svg">
+<g id="Group">
+<g id="Group_2">
+<path id="Vector" d="M2.97139 1.75413C2.97139 0.9336 2.30622 0.268433 1.48569 0.268433C0.665168 0.268433 0 0.9336 0 1.75413C0 2.57465 0.665168 3.23982 1.48569 3.23982C2.30622 3.23982 2.97139 2.57465 2.97139 1.75413Z"/>
+</g>
+<g id="Group_3">
+<path id="Vector_2" d="M8.91426 1.75413C8.91426 0.9336 8.24909 0.268433 7.42856 0.268433C6.60804 0.268433 5.94287 0.9336 5.94287 1.75413C5.94287 2.57465 6.60804 3.23982 7.42856 3.23982C8.24909 3.23982 8.91426 2.57465 8.91426 1.75413Z"/>
+</g>
+<g id="Group_4">
+<path id="Vector_3" d="M2.97139 7.69687C2.97139 6.87635 2.30622 6.21118 1.48569 6.21118C0.665168 6.21118 0 6.87635 0 7.69687C0 8.5174 0.665168 9.18257 1.48569 9.18257C2.30622 9.18257 2.97139 8.5174 2.97139 7.69687Z"/>
+</g>
+<g id="Group_5">
+<path id="Vector_4" d="M8.91426 7.69687C8.91426 6.87635 8.24909 6.21118 7.42856 6.21118C6.60804 6.21118 5.94287 6.87635 5.94287 7.69687C5.94287 8.5174 6.60804 9.18257 7.42856 9.18257C8.24909 9.18257 8.91426 8.5174 8.91426 7.69687Z"/>
+</g>
+<g id="Group_6">
+<path id="Vector_5" d="M2.97139 13.6397C2.97139 12.8192 2.30622 12.154 1.48569 12.154C0.665168 12.154 0 12.8192 0 13.6397C0 14.4602 0.665168 15.1254 1.48569 15.1254C2.30622 15.1254 2.97139 14.4602 2.97139 13.6397Z"/>
+</g>
+<g id="Group_7">
+<path id="Vector_6" d="M8.91426 13.6397C8.91426 12.8192 8.24909 12.154 7.42856 12.154C6.60804 12.154 5.94287 12.8192 5.94287 13.6397C5.94287 14.4602 6.60804 15.1254 7.42856 15.1254C8.24909 15.1254 8.91426 14.4602 8.91426 13.6397Z"/>
+</g>
+</g>
+</svg>

+ 8 - 0
src/assets/svg/search_1.svg

@@ -0,0 +1,8 @@
+<svg width="40" height="41" viewBox="0 0 40 41" xmlns="http://www.w3.org/2000/svg">
+<g id="icon/&#230;&#144;&#156;&#231;&#180;&#162;">
+<g id="Vector">
+<path clip-rule="evenodd" d="M7.45007 19.1764C7.45007 12.2454 13.0688 6.62671 19.9998 6.62671C26.9308 6.62671 32.5495 12.2454 32.5495 19.1764C32.5495 26.1074 26.9308 31.7261 19.9998 31.7261C13.0688 31.7261 7.45007 26.1074 7.45007 19.1764ZM30.0396 19.1764C30.0396 13.6316 25.5446 9.13662 19.9998 9.13662C14.455 9.13662 9.96002 13.6316 9.96002 19.1764C9.96002 24.7212 14.455 29.2162 19.9998 29.2162C25.5446 29.2162 30.0396 24.7212 30.0396 19.1764Z"/>
+<path d="M24.5603 29.5855C24.2138 28.9852 24.4194 28.2177 25.0197 27.8711C25.6199 27.5246 26.3874 27.7303 26.734 28.3305L29.2439 32.6778C29.5905 33.2781 29.3848 34.0456 28.7846 34.3922C28.1843 34.7387 27.4168 34.5331 27.0703 33.9328L24.5603 29.5855Z"/>
+</g>
+</g>
+</svg>

+ 157 - 0
src/components/cus/CusDialog.vue

@@ -0,0 +1,157 @@
+<template>
+  <el-dialog
+      :style="`
+        --cus-dialog_height: ${height};
+        --cus-dialog_max-height: ${maxHeight};
+        --cus-dialog_min-height: ${minHeight};
+      `"
+      :modal-class="`
+        __cus-dialog ${maxHeight === 'unset' ? '' : '__cus-dialog-auto-height'} ${hiddenStyle ? '__cus-dialog-hidden-style' : ''}
+      `"
+      v-bind="$attrs"
+      v-model="showDialog"
+      :title="title"
+      :width="width"
+      align-center
+      :before-close="beforeClose"
+      :show-close="false"
+      :append-to-body="$util.isValue($attrs.appendToBody) ? $attrs.appendToBody : true"
+  >
+    <template #header>
+      <div class="_cus-dialog-head" v-if="title">
+        <div class="__cdh-title">{{title}}</div>
+        <div class="__cdh-slot">
+          <slot name="head"/>
+        </div>
+        <div class="__cdh-close __hover" @click="CDClose()">
+          <SvgIcon name="close_2" size="14"/>
+        </div>
+      </div>
+    </template>
+    <div class="__cus-dialog-main" :class="{isFull: full !== false}" v-loading="loading">
+      <div class="__cus-dialog-content">
+        <slot/>
+      </div>
+      <div class="__cus-dialog-foot" :style="`justify-content: ${footAlign};padding: ${footPadding};`" v-if="showSubmit || showClose || $slots.foot">
+        <slot name="foot" :close="CDClose" :submit="CDSubmit"/>
+        <template v-if="footAlign === 'center'">
+          <div v-if="showSubmit" class="__cus-dialog-foot_submit __hover" @click="CDSubmit">{{submitText}}</div>
+          <div v-if="showClose" class="__cus-dialog-foot_cancel __hover" @click="CDClose()">{{closeText}}</div>
+        </template>
+        <template v-else>
+          <div v-if="showClose" class="__cus-dialog-foot_cancel __hover" @click="CDClose()">{{closeText}}</div>
+          <div v-if="showSubmit" class="__cus-dialog-foot_submit __hover" @click="CDSubmit">{{submitText}}</div>
+        </template>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {ElMessage, ElMessageBox} from "element-plus";
+
+export default defineComponent({
+  name: 'CusDialog',
+  components: {},
+  props: {
+    show: {required: true, type: Boolean},
+    title: {default: ''},
+    width: {default: '50%'},
+    full: {default: false},
+    submitText: {default: '确定'},
+    closeText: {default: '取消'},
+    showClose: {default: true},
+    showSubmit: {default: true},
+    footAlign: {default: 'center'},
+    footPadding: {default: '16px 26px'},
+    height: {default: '60%'},
+    maxHeight: {default: 'unset'},
+    minHeight: {default: 'unset'},
+    closeConfirm: {default: false},
+    closeConfirmText: {default: {
+      title: null,
+      message: null,
+      submit: null,
+      close: null,
+    }},
+    loading: {
+      default: false,
+      type: Boolean
+    },
+    hiddenStyle: {
+      default: false
+    }
+  },
+  setup(props, {emit}) {
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      showDialog: false,
+      closeConfirmTextTemp: {
+        title: '提示',
+        message: '请确认是否关闭?',
+        submit: '确定',
+        close: '取消',
+      }
+    })
+    watch(() => props.show, (n) => {
+      state.showDialog = n
+    })
+    const beforeClose = (done) => {
+      CDClose(done)
+    }
+    const closeConfirmTextCpt: any = computed(() => {
+      return {
+        title: that.$util.isValue(props.closeConfirmText.title) ? props.closeConfirmText.title : state.closeConfirmTextTemp.title,
+        message: that.$util.isValue(props.closeConfirmText.message) ? props.closeConfirmText.message : state.closeConfirmTextTemp.message,
+        submit: that.$util.isValue(props.closeConfirmText.submit) ? props.closeConfirmText.submit : state.closeConfirmTextTemp.submit,
+        close: that.$util.isValue(props.closeConfirmText.close) ? props.closeConfirmText.close : state.closeConfirmTextTemp.close,
+      }
+    })
+    const CDClose = async (done = () => {}) => {
+      if (props.closeConfirm !== false) {
+        await ElMessageBox.confirm(closeConfirmTextCpt.value.message, closeConfirmTextCpt.value.title, {
+          confirmButtonText: closeConfirmTextCpt.value.submit,
+          cancelButtonText: closeConfirmTextCpt.value.close,
+          type: "warning",
+        }).then(() => {
+          emit('update:show', false)
+          emit('onClose')
+          done()
+        }).catch(() => {})
+      } else {
+        emit('update:show', false)
+        emit('onClose')
+        done()
+      }
+    }
+    const CDSubmit = () => {
+      emit('onSubmit')
+    }
+    onMounted(() => {
+      state.showDialog = props.show
+    })
+    return {
+      ...toRefs(state),
+      beforeClose,
+      CDClose,
+      CDSubmit,
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 130 - 0
src/components/cus/CusTab.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="cus-tab" :class="`cus-tab-${type}`">
+    <template v-for="item in tabs">
+      <div class="cus-tab-item __hover" :class="{active: item[valueKey] === param}" @click="$emit('update:param', item[valueKey])">{{item[labelKey]}}</div>
+    </template>
+    <div class="cus-tab-slot">
+      <slot/>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+
+export default defineComponent({
+  name: 'CusTab',
+  components: {},
+  props: {
+    tabs: {
+      required: true
+    },
+    param: {
+      required: true
+    },
+    labelKey: {default: 'name'},
+    valueKey: {default: 'value'},
+    type: {default: 'type1', validator(val: string) {
+      return ['type1', 'type2', 'type3'].includes(val)
+    }},
+  },
+  setup(props, {emit}) {
+    const state = reactive({})
+    return {
+      ...toRefs(state)
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+  .cus-tab {
+    height: 40px;
+    width: 100%;
+    border-bottom: 1px solid #DCDFE5;;
+    display: flex;
+    align-items: center;
+    box-sizing: border-box;
+    .cus-tab-item {
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      font-size: 14px;
+      font-family: PingFang SC-Regular, PingFang SC;
+      font-weight: 400;
+      color: #606266;
+      margin-right: 16px;
+      &:last-child {
+        margin-right: 0;
+      }
+      &.active {
+        &:after {
+          content: '';
+          position: absolute;
+          bottom: 0;
+        }
+      }
+    }
+    &.cus-tab-type1 {
+      .cus-tab-item {
+        padding: 0 4px;
+        &.active {
+          color: #0062E9;
+          &:after {
+            width: 100%;
+            height: 2px;
+            bottom: -1px;
+            background-color: #0062E9;
+          }
+        }
+      }
+    }
+    &.cus-tab-type3 {
+      height: 36px;
+      display: flex;
+      gap: 16px;
+      border-bottom: none;
+      .cus-tab-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        font-family: PingFang SC, PingFang SC;
+        font-weight: 500;
+        font-size: 14px;
+        color: #FFFFFF;
+        margin-right: 0;
+        &:after {
+          margin-top: 6px;
+          content: '';
+          width: 100%;
+          height: 3px;
+          border-radius: 2px;
+          position: unset;
+        }
+        &.active {
+          color: #1CFEFF;
+          &:after {
+            background-color: #1CFEFF;
+          }
+        }
+      }
+    }
+    .cus-tab-slot {
+      margin-left: auto;
+    }
+  }
+</style>

+ 2 - 2
src/plugins/repeatFileValid.ts

@@ -9,11 +9,11 @@ const SvgValid = () => {
     num1++
     // @ts-ignore
     value().then(res => {
-      const regex = /\/([^/]+)\.svg$/
+      const regex = /[^/]+(?=\.[^/.]+)(?![^?=&]+=)/
       for (const [svgKey, svgValue] of Object.entries(res)) {
         // @ts-ignore
         const result: any = svgValue.match(regex);
-        const text = result[1]; // 获取匹配到的文本
+        const text = result[0]; // 获取匹配到的文本
         if (svgRepeatMap.has(text)) {
           svgRepeatMap.set(text, [...svgRepeatMap.get(text), key])
         } else {

+ 1 - 1
src/router/index.ts

@@ -3,7 +3,7 @@ import staticRouter from './modules/static'
 import webRouter from './modules/web'
 import manageRouter from './modules/manage'
 import Temp404 from '@/views/global/temp/404.vue'
-import {mockGetUserInfo} from "@/api/modules/mock/global";
+import {mockGetUserInfo} from "@/api/modules/mock/mock";
 import {ElLoading, ElMessage} from "element-plus";
 import {useAppStore} from "@/stores";
 import {toLogin} from "@/utils/permissions";

+ 1 - 0
src/stores/index.ts

@@ -1,2 +1,3 @@
 export * from './app'
+export * from './web'
 export * from './dictionary'

+ 47 - 0
src/stores/web.ts

@@ -0,0 +1,47 @@
+import {defineStore} from "pinia";
+import {mockGetSearchArea} from "@/api/modules/mock/mock";
+import {readonly} from "vue";
+
+export const useWebStore = defineStore('web', {
+  state: () => ({
+    searchAreaTree: []
+  }),
+  getters: {
+    searchAreaIndexTotal() {
+      let num = 0
+      this.searchAreaTree.forEach(v => {
+        v.children?.forEach(c => {
+          num++
+        })
+      })
+      return num
+    },
+    searchAreaIndexMap() {
+      const map = new Map()
+      this.searchAreaTree.forEach(v => {
+        v.children?.forEach(c => {
+          map.set(c.treeId, c.treeName)
+        })
+      })
+      return map
+    },
+  },
+  actions: {
+    getSearchAreaTree() {
+      return new Promise(resolve => {
+        mockGetSearchArea().then(res => {
+          this.searchAreaTree = readonly(res.data.map(v => {
+            v.treeId = v.name
+            v.treeName = v.name
+            v.children?.forEach(c => {
+              c.treeId = c.indexKey
+              c.treeName = c.indexName
+            })
+            return v
+          }))
+          resolve(this.searchAreaTree)
+        })
+      })
+    }
+  },
+})

+ 152 - 2
src/style/cus.scss

@@ -1,5 +1,16 @@
 :root {
-  --cus-main-color: #CE0022
+  --cus-main-color: #2E81FF;
+  --cus-text-color-1: #303133;
+  --cus-text-color-2: #606266;
+  --cus-text-color-3: #909399;
+  --cus-text-color-4: #C0C4CC;
+}
+
+.__hover {
+  &:hover {
+    opacity: 0.75;
+    cursor: pointer;
+  }
 }
 
 .__tooltip {
@@ -35,4 +46,143 @@
     filter: blur(2px);
     z-index: -1;
   }
-}
+}
+
+.__cus-dialog {
+  &.__cus-dialog-auto-height {
+    .el-overlay-dialog {
+      .el-dialog {
+        max-height: var(--cus-dialog_max-height);
+        min-height: var(--cus-dialog_min-height);
+        height: unset;
+      }
+    }
+  }
+  &.__cus-dialog-hidden-style {
+    .el-dialog__header {
+      display: none;
+    }
+    .__cus-dialog-foot {
+      display: none !important;
+    }
+  }
+  .el-overlay-dialog {
+    .el-dialog {
+      padding: 0;
+      height: var(--cus-dialog_height);
+      $borderRadius: 8px;
+      border-radius: $borderRadius;
+      display: flex;
+      flex-direction: column;
+      .el-dialog__header {
+        padding: 0;
+        margin: 0;
+        ._cus-dialog-head {
+          height: 50px;
+          width: 100%;
+          background-color: #F5F7FA;
+          display: flex;
+          align-items: center;
+          border-radius: $borderRadius $borderRadius 0 0 ;
+          font-size: 18px;
+          font-family: PingFang SC-Medium, PingFang SC;
+          font-weight: 500;
+          color: #303133;
+          padding: 0 20px 0 16px;
+          box-sizing: border-box;
+          .__cdh-title {}
+          .__cdh-slot {}
+          .__cdh-close {
+            margin-left: auto;
+          }
+        }
+      }
+      .el-dialog__body {
+        padding: 0;
+        height: calc(100% - 50px);
+        width: 100%;
+        display: flex;
+        overflow-y: hidden;
+        flex: 1;
+        .__cus-dialog-main {
+          width: 100%;
+          display: flex;
+          flex-direction: column;
+          .__cus-dialog-content {
+            flex: 1;
+            overflow-y: auto;
+          }
+          .__cus-dialog-foot {
+            width: 100%;
+            display: flex;
+            align-items: center;
+            box-sizing: border-box;
+            >div {
+              margin-right: 10px;
+              height: 28px;
+              border-radius: 28px;
+              padding: 0 14px;
+              line-height: 1;
+              font-size: 14px;
+              font-family: Microsoft YaHei;
+              font-weight: 400;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              box-sizing: border-box;
+              &:last-child {
+                margin-right: 0;
+              }
+            }
+            .__cus-dialog-foot_submit {
+              background: var(--cus-main-color);
+              color: #FFFFFF;
+            }
+            .__cus-dialog-foot_cancel {
+              background: #F8F8F8;
+              border: 1px solid #E4E4E4;
+              color: var(--cus-text-color-3);
+            }
+          }
+          &.isFull {
+            overflow-y: auto;
+            .__cus-dialog-content {
+              overflow-y: unset;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.__check {
+  display: flex;
+  align-items: center;
+  position: relative;
+  &:hover {
+    opacity: 0.75;
+    cursor: pointer;
+  }
+  &:before {
+    content: '';
+    width: 16px;
+    height: 16px;
+    box-sizing: border-box;
+    border: 1px solid #d3d3d3;
+    border-radius: 2px;
+    cursor: pointer;
+    margin-right: 4px;
+  }
+  &.active {
+    &:before {
+      background-color: var(--cus-main-color);
+      border-color: var(--cus-main-color);
+      content: '✔';
+      color: #FFFFFF;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}

+ 224 - 8
src/views/web/home/index.vue

@@ -4,31 +4,144 @@
       <img src="@/assets/images/web/web-home_title.png"/>
     </div>
     <div class="area">
-
       <div class="selected">
         <div class="label">搜索范围:</div>
-        <div class="value"></div>
+        <div class="value">
+          <template v-if="searchAreaCpt.text">
+            {{searchAreaCpt.text}}
+          </template>
+          <template v-else>
+            <template v-for="item in searchAreaCpt.arr">
+              <div>{{item.indexName}}</div>
+            </template>
+          </template>
+        </div>
       </div>
       <div class="search-input">
-        <el-input v-model="state.searchText" placeholder="请输入关键字进行查询"/>
+        <div class="left-select __hover" @click="state.showArea = true">
+          搜索范围<SvgIcon name="arrow_1" rotate="90" size="14" color="var(--cus-text-color-3)"/>
+        </div>
+        <el-input v-model="state.searchText" placeholder="请输入关键字进行查询" @keyup.enter="toList(state.searchText)"/>
+        <div class="right-icon __hover" @click="toList(state.searchText)">
+          <SvgIcon name="search_1" color="var(--cus-main-color)" size="40"/>
+        </div>
       </div>
     </div>
-    <div class="history">
+    <div class="history" v-if="state.historyList?.length > 0">
       <div class="label">搜索记录</div>
       <div class="result">
-
+        <template v-for="(item, index) in state.historyList">
+          <span class="__hover" @click="toList(item, true)">{{index + 1}}.{{item}}</span>
+        </template>
       </div>
     </div>
-<!--    <el-button @click="$router.push({name: '4f6dd2ea-7c0a-4923-9a57-932ef42235f6'})">跳转到列表</el-button>-->
+    <CusDialog
+      :show="state.showArea"
+      @onClose="$emit('update:show', false)"
+      width="1000px"
+      height="auto"
+      submit-text="关闭"
+      :show-close="false"
+      @onSubmit="state.showArea = false"
+    >
+      <CusTab :tabs="state.areaList" type="type1" v-model:param="state.areaTab" label-key="treeName" value-key="treeId"/>
+      <div class="index-list">
+        <div class="all">
+          <div class="__check" :class="{active: indexTabAllCpt}" @click="onIndexTabAll">全选</div>
+        </div>
+        <div class="list">
+          <template v-for="(item, index) in indexListCpt">
+            <div class="list-item">
+              <div class="__check" :class="{active: item.__select}" @click="item.__select = !item.__select">{{ item.indexName }}</div>
+            </div>
+          </template>
+        </div>
+      </div>
+    </CusDialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import {getCurrentInstance, reactive} from "vue";
+import {computed, getCurrentInstance, onMounted, reactive} from "vue";
+import router from "@/router";
+import {ElMessage} from "element-plus";
+import {useWebStore} from "@/stores";
 
 const {proxy} = getCurrentInstance()
+const WebStore = useWebStore()
 const state: any = reactive({
-  searchText: ''
+  searchText: '',
+  historyList: [],
+  areaList: [],
+  showArea: false,
+  areaTab: ''
+})
+const indexListCpt = computed(() => {
+  return state.areaList.filter(v => v.treeId === state.areaTab)?.[0]?.children || []
+})
+const indexTabAllCpt = computed(() => {
+  return indexListCpt.value.every(v => v.__select)
+})
+const searchAreaCpt = computed(() => {
+  const obj = {
+    text: '',
+    arr: []
+  }
+  let i = 0
+  state.areaList.forEach(v => {
+    v.children.forEach(c => {
+      i++
+      if (c.__select) {
+        obj.arr.push(c)
+      }
+    })
+  })
+  if (i === obj.arr.length) {
+    obj.arr = []
+  }
+  if (obj.arr.length === 0) {
+    obj.text = '全部'
+  }
+  return obj
+})
+const initHistory = () => {
+  proxy.$api.mockGetSearchHistory().then(res => {
+    state.historyList = res.data
+  })
+}
+const initArea = () => {
+  WebStore.getSearchAreaTree().then(res => {
+    state.areaList = JSON.parse(JSON.stringify(res))
+    state.areaTab = state.areaList[0].treeId
+  })
+}
+const onIndexTabAll = () => {
+  const flag = JSON.parse(JSON.stringify(indexTabAllCpt.value))
+  indexListCpt.value.forEach(v => {
+    v.__select = !flag
+  })
+}
+const toList = (text, isAll = false) => {
+  if (text) {
+    const routerUrl = router.resolve({
+      name: '4f6dd2ea-7c0a-4923-9a57-932ef42235f6',
+      query: {
+        text,
+        index: isAll ? '' : searchAreaCpt.value.arr.map(v => v.indexKey).join(',')
+      }
+    });
+    window.open(routerUrl.href, "_blank");
+  } else {
+    ElMessage({
+      message: '请输入关键字进行查询!',
+      grouping: true,
+      type: 'warning',
+    })
+  }
+}
+onMounted(() => {
+  initHistory()
+  initArea()
 })
 </script>
 
@@ -44,5 +157,108 @@ const state: any = reactive({
   justify-content: center;
   align-items: center;
   gap: 40px;
+  .area {
+    width: 1000px;
+    .selected {
+      display: flex;
+      line-height: 30px;
+      .label {
+        font-weight: 500;
+        font-size: 16px;
+        color: var(--cus-main-color);
+        padding-left: 16px;
+      }
+      .value {
+        flex: 1;
+        color: var(--cus-text-color-2);
+        display: flex;
+        flex-wrap: wrap;
+        column-gap: 20px;
+      }
+    }
+    .search-input {
+      margin-top: 10px;
+      display: flex;
+      align-items: center;
+      width: 100%;
+      height: 60px;
+      background: rgba(255,255,255,0.9);
+      box-shadow: 0px 0px 2px 0px rgba(167,220,255,0.5);
+      border-radius: 60px;
+      border: 1px solid var(--cus-main-color);
+      .left-select {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 184px;
+        gap: 17px;
+        color: var(--cus-text-color-4);
+        font-size: 18px;
+        position: relative;
+        &:after {
+          content: '';
+          position: absolute;
+          right: 0;
+          height: 40px;
+          width: 1px;
+          background-color: var(--cus-main-color);
+        }
+      }
+      :deep(.el-input) {
+        font-size: 18px;
+        color: var(--cus-text-color-2);
+        .el-input__wrapper {
+          box-shadow: none;
+          background-color: transparent;
+          .el-input__inner {
+            &::placeholder {
+              color: var(--cus-text-color-4);
+            }
+          }
+        }
+      }
+      .right-icon {
+        margin-right: 20px;
+      }
+    }
+  }
+  .history {
+    width: 1000px;
+    .label {
+      font-weight: 500;
+      font-size: 16px;
+      color: var(--cus-main-color);
+      padding-left: 16px;
+    }
+    .result {
+      margin-top: 10px;
+      width: 100%;
+      padding: 16px;
+      background: rgba(255,255,255,0.8);
+      box-shadow: 0px 4px 4px 0px rgba(255,255,255,0.15);
+      border-radius: 8px;
+      font-weight: 400;
+      font-size: 16px;
+      color: var(--cus-text-color-2);
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px 32px;
+    }
+  }
+}
+.cus-tab {
+  padding: 30px 0 22px 28px;
+  :deep(.cus-tab-item::after) {
+    bottom: -23px !important;
+  }
+}
+.index-list {
+  padding: 20px 28px 0;
+  .list {
+    margin-top: 10px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 30px;
+  }
 }
 </style>

+ 283 - 4
src/views/web/list/index.vue

@@ -1,15 +1,294 @@
 <template>
-  <div>
-    <h1>这是智搜列表页</h1>
+  <div class="list">
+    <div class="list-head">
+      <div class="list-head-search">
+        <div class="left-select __hover" @click.stop="ref_area.togglePopperVisible(true)">
+          搜索范围<SvgIcon name="arrow_1" rotate="90" size="14" color="var(--cus-text-color-3)"/>
+          <el-cascader ref="ref_area" class="area-cascader" v-model="state.cascaderParams.value" :props="state.cascaderParams.props" :options="state.cascaderParams.options"/>
+        </div>
+        <el-input v-model="state.searchText" placeholder="请输入关键字进行查询" @keyup.enter="onSearch"/>
+        <div class="right-icon __hover" @click="onSearch">
+          <SvgIcon name="search_1" color="var(--cus-main-color)" size="40"/>
+        </div>
+      </div>
+      <div class="list-head-user">
+        <div class="avatar">
+          <img src="@/assets/images/web/web-list_avatar.png"/>
+        </div>
+        用户名
+      </div>
+    </div>
+    <div class="list-content">
+      <div class="list-filter">
+        <div class="label">检索条件:</div>
+        <div class="value">
+          <template v-if="isSelectAllCpt || !state.cascaderParams.value.length">
+            <div class="filter-item">全部</div>
+          </template>
+          <template v-else>
+            <template v-for="(item, index) in state.cascaderParams.value">
+              <div class="filter-item">
+                {{ WebStore.searchAreaIndexMap.get(item) }}
+                <SvgIcon class="__hover" name="close_1" size="10" color="var(--cus-text-color-3)" @click="onDelFilter(index)"/>
+              </div>
+            </template>
+          </template>
+        </div>
+      </div>
+      <div class="list-tab">
+        <template v-for="item in state.resultParams.tree">
+          <div class="list-tab-item __hover" :class="{active: item.treeId === state.resultParams.activeTab}">{{item.treeName}}<span class="total">(898989898989)</span></div>
+        </template>
+      </div>
+      <div class="list-result">
+        <div class="list-result-tree"></div>
+        <div class="list-result-table"></div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import {getCurrentInstance, reactive} from "vue";
+import {computed, getCurrentInstance, onMounted, reactive, ref} from "vue";
+import {useRoute, useRouter} from "vue-router";
+import {useWebStore} from "@/stores";
 
 const {proxy} = getCurrentInstance()
-const state: any = reactive({})
+const WebStore = useWebStore()
+const route = useRoute()
+const router = useRouter()
+const state: any = reactive({
+  searchText: '',
+  cascaderParams: {
+    value: [],
+    props: {
+      label: 'treeName',
+      value: 'treeId',
+      multiple: true,
+      emitPath: false
+    },
+    show: false,
+    options: []
+  },
+  searchParams: {
+    text: '',
+    indexKey: []
+  },
+  resultParams: {
+    tree: [],
+    activeTab: '',
+    activeIndex: '',
+  }
+})
+const ref_area = ref()
+const isSelectAllCpt = computed(() => {
+  return WebStore.searchAreaIndexTotal === state.cascaderParams.value.length
+})
+const initArea = () => {
+  WebStore.getSearchAreaTree().then(res => {
+    state.cascaderParams.options = res
+    onSearch()
+  })
+}
+const onDelFilter = (index) => {
+  const temp = JSON.parse(JSON.stringify(state.cascaderParams.value))
+  temp.splice(index, 1)
+  state.cascaderParams.value = temp
+}
+const onSearch = () => {
+  state.searchParams = JSON.parse(JSON.stringify({
+    text: state.searchText,
+    indexKey: state.cascaderParams.value
+  }))
+  initResultTree()
+}
+const initResultTree = () => {
+  const arr = []
+  WebStore.searchAreaTree.forEach(v => {
+    const t = {
+      ...v,
+      children: []
+    }
+    v.children?.forEach(c => {
+      if (state.searchParams.indexKey.includes(c.treeId)) {
+        t.children.push(c)
+      }
+    })
+    if (t.children.length > 0) {
+      arr.push(t)
+    }
+  })
+  if (arr.length > 0) {
+    state.resultParams.tree = arr
+  } else {
+    state.resultParams.tree = JSON.parse(JSON.stringify(WebStore.searchAreaTree))
+  }
+  state.resultParams.activeTab = state.resultParams.tree[0].treeId
+  state.resultParams.activeIndex = state.resultParams.tree[0].children[0].treeId
+}
+onMounted(() => {
+  initArea()
+  const {text, index} = route.query
+  if (!text) {
+    router.replace({
+      name: '71305311-abcc-4d13-8531-f966b49839c5'
+    })
+  } else {
+    state.searchText = text
+    if (index) {
+      state.cascaderParams.value = index.split(',')
+    }
+  }
+})
 </script>
 
 <style lang="scss" scoped>
+.list {
+  width: 100%;
+  height: 100%;
+  background-color: #F6F7FB;
+  display: flex;
+  flex-direction: column;
+  .list-head {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 72px;
+    background-color: #FFFFFF;
+    .list-head-search {
+      margin-left: auto;
+      width: 800px;
+      display: flex;
+      align-items: center;
+      height: 48px;
+      background: rgba(255,255,255,0.9);
+      box-shadow: 0px 0px 2px 0px rgba(167,220,255,0.5);
+      border-radius: 60px;
+      border: 1px solid var(--cus-text-color-4);
+      .left-select {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 184px;
+        gap: 17px;
+        color: var(--cus-text-color-4);
+        font-size: 18px;
+        position: relative;
+        &:after {
+          content: '';
+          position: absolute;
+          right: 0;
+          height: 40px;
+          width: 1px;
+          background-color: var(--cus-text-color-4);
+        }
+        :deep(.area-cascader) {
+          position: absolute;
+          z-index: -1;
+          opacity: 0;
+          height: 50px;
+        }
+      }
+      :deep(.el-input) {
+        font-size: 18px;
+        color: var(--cus-text-color-2);
+        .el-input__wrapper {
+          box-shadow: none;
+          background-color: transparent;
+          .el-input__inner {
+            &::placeholder {
+              color: var(--cus-text-color-4);
+            }
+          }
+        }
+      }
+      .right-icon {
+        margin-right: 20px;
+      }
+    }
+    .list-head-user {
+      display: flex;
+      align-items: center;
+      color: var(--cus-main-color);
+      margin: 0 24px;
+      .avatar {
+        margin-right: 8px;
+      }
+    }
+  }
+  .list-content {
+    flex: 1;
+    padding: 24px;
+    display: flex;
+    flex-direction: column;
+    $filterLineHeight: 26px;
+    .list-filter {
+      font-weight: 500;
+      font-size: 14px;
+      color: var(--cus-text-color-2);
+      display: flex;
+      .label {
+        margin-right: 20px;
+        line-height: $filterLineHeight;
+      }
+      .value {
+        flex: 1;
+        display: flex;
+        flex-wrap: wrap;
+        gap: 10px 10px;
+        .filter-item {
+          height: $filterLineHeight;
+          padding: 0 16px;
+          background: #FFFFFF;
+          border-radius: 45px;
+          font-size: 12px;
+          display: flex;
+          align-items: center;
+          .svg-icon {
+            margin-left: 10px;
+          }
+        }
+      }
+    }
+    .list-tab {
+      height: 45px;
+      margin-top: 19px;
+      display: flex;
+      .list-tab-item {
+        height: 100%;
+        padding: 0 16px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: var(--cus-text-color-2);
+        border-radius: 8px 8px 0 0;
+        .total {
+          font-size: 12px;
+          margin-left: 4px;
+          color: var(--cus-text-color-2);
+          font-weight: 400;
+        }
+        &.active {
+          background-color: #ffffff;
+          box-shadow: 0px -4px 10px 0px rgba(62,123,250,0.1);
+          color: var(--cus-main-color);
+          font-weight: 500;
+          position: relative;
+          &:after {
+            content: '';
+            position: absolute;
+            bottom: 0;
+            width: calc(100% - 16px * 2);
+            height: 3px;
+            background-color: var(--cus-main-color);
+          }
+        }
+      }
+    }
+    .list-result {
+      flex: 1;
+      background-color: #ffffff;
+    }
+  }
+}
 </style>