Browse Source

列表页搜索

CzRger 7 months ago
parent
commit
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',
+})

BIN
src/assets/images/web/web-list_avatar.png


File diff suppressed because it is too large
+ 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>

File diff suppressed because it is too large
+ 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>