mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	Merge branch 'main' of github.com:pure-admin/vue-pure-admin into gitee
This commit is contained in:
		
						commit
						93498b6643
					
				@ -13,7 +13,7 @@
 | 
			
		||||
    "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
 | 
			
		||||
    "svgo": "svgo -f src/assets/svg -o src/assets/svg",
 | 
			
		||||
    "cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
 | 
			
		||||
    "clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
 | 
			
		||||
    "clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
 | 
			
		||||
    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
 | 
			
		||||
    "lint:prettier": "prettier --write  \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
 | 
			
		||||
    "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
 | 
			
		||||
 | 
			
		||||
@ -105,7 +105,8 @@ watch(
 | 
			
		||||
        props.modelValue.indexOf(":") + 1
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
watch(
 | 
			
		||||
  () => {
 | 
			
		||||
@ -140,12 +141,11 @@ watch(
 | 
			
		||||
          </template>
 | 
			
		||||
 | 
			
		||||
          <el-input
 | 
			
		||||
            class="p-2"
 | 
			
		||||
            class="px-2 pt-2"
 | 
			
		||||
            v-model="filterValue"
 | 
			
		||||
            placeholder="搜索图标"
 | 
			
		||||
            clearable
 | 
			
		||||
          />
 | 
			
		||||
          <el-divider border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
          <el-tabs v-model="currentActiveType" @tab-click="handleClick">
 | 
			
		||||
            <el-tab-pane
 | 
			
		||||
@ -154,24 +154,26 @@ watch(
 | 
			
		||||
              :label="pane.label"
 | 
			
		||||
              :name="pane.name"
 | 
			
		||||
            >
 | 
			
		||||
              <el-divider class="tab-divider" border-style="dashed" />
 | 
			
		||||
              <el-scrollbar height="220px">
 | 
			
		||||
                <ul class="flex flex-wrap px-2 ml-2">
 | 
			
		||||
                  <li
 | 
			
		||||
                    v-for="(item, key) in pageList"
 | 
			
		||||
                    :key="key"
 | 
			
		||||
                    :title="item"
 | 
			
		||||
                    class="icon-item p-2 w-[1/10] cursor-pointer mr-2 mt-1 flex justify-center items-center border border-solid"
 | 
			
		||||
                    class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-solid"
 | 
			
		||||
                    :style="iconItemStyle(item)"
 | 
			
		||||
                    @click="onChangeIcon(item)"
 | 
			
		||||
                  >
 | 
			
		||||
                    <IconifyIconOnline :icon="currentActiveType + item" />
 | 
			
		||||
                    <IconifyIconOnline
 | 
			
		||||
                      :icon="currentActiveType + item"
 | 
			
		||||
                      width="20px"
 | 
			
		||||
                      height="20px"
 | 
			
		||||
                    />
 | 
			
		||||
                  </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
              </el-scrollbar>
 | 
			
		||||
            </el-tab-pane>
 | 
			
		||||
          </el-tabs>
 | 
			
		||||
          <el-divider border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
          <el-pagination
 | 
			
		||||
            small
 | 
			
		||||
@ -190,14 +192,6 @@ watch(
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.el-divider--horizontal {
 | 
			
		||||
  margin: 1px auto !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tab-divider.el-divider--horizontal {
 | 
			
		||||
  margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-item {
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: var(--el-color-primary);
 | 
			
		||||
@ -234,5 +228,10 @@ watch(
 | 
			
		||||
:deep(.el-tabs__nav-wrap) {
 | 
			
		||||
  position: static;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.06);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:deep(.el-tabs__content) {
 | 
			
		||||
  margin-top: 4px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
			
		||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
			
		||||
import { useNav } from "@/layout/hooks/useNav";
 | 
			
		||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{ total: number }>(), {
 | 
			
		||||
  total: 0
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { device } = useNav();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="search-footer text-[#333] dark:text-white">
 | 
			
		||||
    <span class="search-footer-item">
 | 
			
		||||
@ -13,16 +27,15 @@
 | 
			
		||||
      <mdiKeyboardEsc class="icon" />
 | 
			
		||||
      关闭
 | 
			
		||||
    </span>
 | 
			
		||||
    <p
 | 
			
		||||
      v-if="device !== 'mobile' && props.total > 0"
 | 
			
		||||
      class="search-footer-total"
 | 
			
		||||
    >
 | 
			
		||||
      共{{ props.total }}项
 | 
			
		||||
    </p>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
			
		||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
			
		||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.search-footer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
    box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
 | 
			
		||||
      0 1px 2px 1px #1e235a66;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search-footer-total {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import { transformI18n } from "@/plugins/i18n";
 | 
			
		||||
import { ref, computed, shallowRef } from "vue";
 | 
			
		||||
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 | 
			
		||||
import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
			
		||||
import Search from "@iconify-icons/ep/search";
 | 
			
		||||
import Search from "@iconify-icons/ri/search-line";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  /** 弹窗显隐 */
 | 
			
		||||
@ -25,6 +25,8 @@ const props = withDefaults(defineProps<Props>(), {});
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const keyword = ref("");
 | 
			
		||||
const scrollbarRef = ref();
 | 
			
		||||
const resultRef = ref();
 | 
			
		||||
const activePath = ref("");
 | 
			
		||||
const inputRef = ref<HTMLInputElement | null>(null);
 | 
			
		||||
const resultOptions = shallowRef([]);
 | 
			
		||||
@ -83,6 +85,11 @@ function handleClose() {
 | 
			
		||||
  }, 200);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function scrollTo(index) {
 | 
			
		||||
  const scrollTop = resultRef.value.handleScroll(index);
 | 
			
		||||
  scrollbarRef.value.setScrollTop(scrollTop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** key up */
 | 
			
		||||
function handleUp() {
 | 
			
		||||
  const { length } = resultOptions.value;
 | 
			
		||||
@ -92,8 +99,10 @@ function handleUp() {
 | 
			
		||||
  );
 | 
			
		||||
  if (index === 0) {
 | 
			
		||||
    activePath.value = resultOptions.value[length - 1].path;
 | 
			
		||||
    scrollTo(resultOptions.value.length - 1);
 | 
			
		||||
  } else {
 | 
			
		||||
    activePath.value = resultOptions.value[index - 1].path;
 | 
			
		||||
    scrollTo(index - 1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -109,6 +118,7 @@ function handleDown() {
 | 
			
		||||
  } else {
 | 
			
		||||
    activePath.value = resultOptions.value[index + 1].path;
 | 
			
		||||
  }
 | 
			
		||||
  scrollTo(index + 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** key enter */
 | 
			
		||||
@ -127,41 +137,55 @@ onKeyStroke("ArrowDown", handleDown);
 | 
			
		||||
<template>
 | 
			
		||||
  <el-dialog
 | 
			
		||||
    top="5vh"
 | 
			
		||||
    class="pure-search-dialog"
 | 
			
		||||
    v-model="show"
 | 
			
		||||
    :width="device === 'mobile' ? '80vw' : '50vw'"
 | 
			
		||||
    :show-close="false"
 | 
			
		||||
    :width="device === 'mobile' ? '80vw' : '40vw'"
 | 
			
		||||
    :before-close="handleClose"
 | 
			
		||||
    :style="{
 | 
			
		||||
      borderRadius: '6px'
 | 
			
		||||
    }"
 | 
			
		||||
    @opened="inputRef.focus()"
 | 
			
		||||
    @closed="inputRef.blur()"
 | 
			
		||||
  >
 | 
			
		||||
    <el-input
 | 
			
		||||
      ref="inputRef"
 | 
			
		||||
      size="large"
 | 
			
		||||
      v-model="keyword"
 | 
			
		||||
      clearable
 | 
			
		||||
      placeholder="请输入关键词搜索"
 | 
			
		||||
      placeholder="搜索菜单"
 | 
			
		||||
      @input="handleSearch"
 | 
			
		||||
    >
 | 
			
		||||
      <template #prefix>
 | 
			
		||||
        <span class="el-input__icon">
 | 
			
		||||
          <IconifyIconOffline :icon="Search" />
 | 
			
		||||
        </span>
 | 
			
		||||
        <IconifyIconOffline
 | 
			
		||||
          :icon="Search"
 | 
			
		||||
          class="text-primary w-[24px] h-[24px]"
 | 
			
		||||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-input>
 | 
			
		||||
    <div class="search-result-container">
 | 
			
		||||
      <el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
 | 
			
		||||
      <SearchResult
 | 
			
		||||
        v-else
 | 
			
		||||
        v-model:value="activePath"
 | 
			
		||||
        :options="resultOptions"
 | 
			
		||||
        @click="handleEnter"
 | 
			
		||||
      />
 | 
			
		||||
      <el-scrollbar ref="scrollbarRef" max-height="600px">
 | 
			
		||||
        <el-empty
 | 
			
		||||
          v-if="resultOptions.length === 0"
 | 
			
		||||
          description="暂无搜索结果"
 | 
			
		||||
        />
 | 
			
		||||
        <SearchResult
 | 
			
		||||
          v-else
 | 
			
		||||
          ref="resultRef"
 | 
			
		||||
          v-model:value="activePath"
 | 
			
		||||
          :options="resultOptions"
 | 
			
		||||
          @click="handleEnter"
 | 
			
		||||
        />
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
    </div>
 | 
			
		||||
    <template #footer>
 | 
			
		||||
      <SearchFooter />
 | 
			
		||||
      <SearchFooter :total="resultOptions.length" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </el-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.search-result-container {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  margin-top: 12px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import { computed, getCurrentInstance } from "vue";
 | 
			
		||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
 | 
			
		||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
@ -28,6 +28,7 @@ interface Emits {
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {});
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
const instance = getCurrentInstance()!;
 | 
			
		||||
 | 
			
		||||
const itemStyle = computed(() => {
 | 
			
		||||
  return item => {
 | 
			
		||||
@ -57,22 +58,33 @@ async function handleMouse(item) {
 | 
			
		||||
function handleTo() {
 | 
			
		||||
  emit("enter");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleScroll(index: number) {
 | 
			
		||||
  const curInstance = instance?.proxy?.$refs[`resultItemRef${index}`];
 | 
			
		||||
  if (!curInstance) return 0;
 | 
			
		||||
  const curRef = curInstance[0] as ElRef;
 | 
			
		||||
  const scrollTop = curRef.offsetTop + 128; // 128 两个result-item(56px+56px=112px)高度加上下margin(8px+8px=16px)
 | 
			
		||||
  return scrollTop > 600 ? scrollTop - 600 : 0; // 600 el-scrollbar max-height="600px"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ handleScroll });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="result">
 | 
			
		||||
    <template v-for="item in options" :key="item.path">
 | 
			
		||||
      <div
 | 
			
		||||
        class="result-item dark:bg-[#1d1d1d]"
 | 
			
		||||
        :style="itemStyle(item)"
 | 
			
		||||
        @click="handleTo"
 | 
			
		||||
        @mouseenter="handleMouse(item)"
 | 
			
		||||
      >
 | 
			
		||||
        <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
 | 
			
		||||
        <span class="result-item-title">{{ t(item.meta?.title) }}</span>
 | 
			
		||||
        <enterOutlined />
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="(item, index) in options"
 | 
			
		||||
      :key="item.path"
 | 
			
		||||
      :ref="'resultItemRef' + index"
 | 
			
		||||
      class="result-item dark:bg-[#1d1d1d]"
 | 
			
		||||
      :style="itemStyle(item)"
 | 
			
		||||
      @click="handleTo"
 | 
			
		||||
      @mouseenter="handleMouse(item)"
 | 
			
		||||
    >
 | 
			
		||||
      <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
 | 
			
		||||
      <span class="result-item-title">{{ t(item.meta?.title) }}</span>
 | 
			
		||||
      <enterOutlined />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -139,6 +139,23 @@ html.dark {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 自定义菜单搜索样式 */
 | 
			
		||||
  .pure-search-dialog {
 | 
			
		||||
    .el-dialog__footer {
 | 
			
		||||
      box-shadow: 0 -1px 0 0 #555a64, 0 -3px 6px 0 rgb(69 98 155 / 12%);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .search-footer {
 | 
			
		||||
      .search-footer-item {
 | 
			
		||||
        color: rgb(235 235 235 / 60%);
 | 
			
		||||
 | 
			
		||||
        .icon {
 | 
			
		||||
          box-shadow: none;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* ReSegmented 组件 */
 | 
			
		||||
  .pure-segmented {
 | 
			
		||||
    color: rgb(255 255 255 / 65%);
 | 
			
		||||
 | 
			
		||||
@ -148,3 +148,24 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 自定义菜单搜索样式 */
 | 
			
		||||
.pure-search-dialog {
 | 
			
		||||
  .el-dialog__header {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-dialog__body {
 | 
			
		||||
    padding-top: 12px;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-input__inner {
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-dialog__footer {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,12 @@ export const useVerifyCode = () => {
 | 
			
		||||
    await formEl.validateField(props, isValid => {
 | 
			
		||||
      if (isValid) {
 | 
			
		||||
        clearInterval(timer.value);
 | 
			
		||||
        isDisabled.value = true;
 | 
			
		||||
        text.value = `${time}`;
 | 
			
		||||
        timer.value = setInterval(() => {
 | 
			
		||||
          if (time > 0) {
 | 
			
		||||
            text.value = `${time}`;
 | 
			
		||||
            isDisabled.value = true;
 | 
			
		||||
            time -= 1;
 | 
			
		||||
            text.value = `${time}`;
 | 
			
		||||
          } else {
 | 
			
		||||
            text.value = "";
 | 
			
		||||
            isDisabled.value = false;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user