Sanakey
2024-08-02 1ba88a00d3c7bb0f16a73feaa5ffcf7c1635e0ea
Merge remote-tracking branch 'origin/v2' into onbus-crm
1个文件已添加
16个文件已修改
226 ■■■■ 已修改文件
.husky/commit-msg 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.husky/pre-commit 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Form/src/components/ApiSelect.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Loading/src/Loading.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Preview/src/functional.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Scrollbar/src/Scrollbar.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/components/editable/EditableCell.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/components/UploadModal.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/design/public.less 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useECharts.ts 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/components/UpgradePrompt.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/default/header/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/en/layout.json 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/locales/lang/zh-CN/layout.json 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/logics/initAppConfig.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/multipleTab.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/form/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.husky/commit-msg
@@ -5,4 +5,4 @@
PATH="/usr/local/bin:$PATH"
npx --no-install commitlint --edit "$1"
# npx --no-install commitlint --edit "$1"
.husky/pre-commit
@@ -7,4 +7,4 @@
PATH="/usr/local/bin:$PATH"
# Format and submit code according to lintstagedrc.js configuration
pnpm exec lint-staged
# pnpm exec lint-staged
src/components/Form/src/components/ApiSelect.vue
@@ -3,6 +3,7 @@
    @dropdown-visible-change="handleFetch"
    v-bind="$attrs"
    @change="handleChange"
    @search="debounceSearchFn"
    :options="getOptions"
    v-model:value="state"
  >
@@ -20,18 +21,33 @@
    </template>
  </Select>
</template>
<script lang="ts" setup>
  import { PropType, ref, computed, unref, watch } from 'vue';
  import { computed, PropType, ref, unref, watch } from 'vue';
  import { Select } from 'ant-design-vue';
  import type { SelectValue } from 'ant-design-vue/es/select';
  import { isFunction } from '@/utils/is';
  import { isEmpty, isFunction } from '@/utils/is';
  import { useRuleFormItem } from '@/hooks/component/useFormItem';
  import { get, omit, isEqual } from 'lodash-es';
  import { assignIn, get, isEqual, omit } from 'lodash-es';
  import { LoadingOutlined } from '@ant-design/icons-vue';
  import { useI18n } from '@/hooks/web/useI18n';
  import { propTypes } from '@/utils/propTypes';
  import { useDebounceFn } from '@vueuse/core';
  type OptionsItem = { label?: string; value?: string; disabled?: boolean; [name: string]: any };
  type ApiSearchOption = {
    // 展示搜索
    show?: boolean;
    // 待搜索字段名
    searchName?: string;
    // 是否允许空搜索
    emptySearch?: boolean;
    // 搜索前置方法
    beforeFetch?: (value?: string) => Promise<string>;
    // 拦截方法
    interceptFetch?: (value?: string) => Promise<boolean>;
  };
  defineOptions({ name: 'ApiSelect', inheritAttrs: false });
@@ -39,7 +55,7 @@
    value: { type: [Array, Object, String, Number] as PropType<SelectValue> },
    numberToString: propTypes.bool,
    api: {
      type: Function as PropType<(arg?: any) => Promise<OptionsItem[] | Recordable<any>>>,
      type: Function as PropType<(arg?: any) => Promise<OptionsItem[] | Recordable>>,
      default: null,
    },
    // api params
@@ -53,6 +69,10 @@
    options: {
      type: Array<OptionsItem>,
      default: [],
    },
    apiSearch: {
      type: Object as PropType<ApiSearchOption>,
      default: () => null,
    },
    beforeFetch: {
      type: Function as PropType<Fn>,
@@ -72,6 +92,7 @@
  // 首次是否加载过了
  const isFirstLoaded = ref(false);
  const emitData = ref<OptionsItem[]>([]);
  const searchParams = ref<any>({});
  const { t } = useI18n();
  // Embedded in the form, just use the hook binding to perform form verification
@@ -110,16 +131,29 @@
    { deep: true, immediate: props.immediate },
  );
  watch(
    () => searchParams.value,
    (value, oldValue) => {
      if (isEmpty(value) || isEqual(value, oldValue)) return;
      (async () => {
        await fetch();
        searchParams.value = {};
      })();
    },
    { deep: true, immediate: props.immediate },
  );
  async function fetch() {
    let { api, beforeFetch, afterFetch, params, resultField } = props;
    if (!api || !isFunction(api) || loading.value) return;
    optionsRef.value = [];
    try {
      loading.value = true;
      let apiParams = assignIn({}, params, searchParams.value);
      if (beforeFetch && isFunction(beforeFetch)) {
        params = (await beforeFetch(params)) || params;
        apiParams = (await beforeFetch(apiParams)) || apiParams;
      }
      let res = await api(params);
      let res = await api(apiParams);
      if (afterFetch && isFunction(afterFetch)) {
        res = (await afterFetch(res)) || res;
      }
@@ -147,11 +181,43 @@
      if (props.alwaysLoad) {
        await fetch();
      } else if (!props.immediate && !unref(isFirstLoaded)) {
        await fetch();
        // 动态搜索查询时,允许控制初始不加载数据
        if (!(!!props.apiSearch && !!props.apiSearch.show && !props.apiSearch.emptySearch)) {
          await fetch();
        } else {
          optionsRef.value = [];
          emitChange();
        }
      }
    }
  }
  let debounceSearchFn = useDebounceFn(handleSearch, 500);
  async function handleSearch(value: any) {
    if (!props.apiSearch) {
      return;
    }
    const { show, searchName, beforeFetch, interceptFetch } = props.apiSearch;
    if (!show || !searchName) {
      return;
    }
    value = value || undefined;
    if (beforeFetch && isFunction(beforeFetch)) {
      value = (await beforeFetch(value)) || value;
    }
    if (interceptFetch && isFunction(interceptFetch)) {
      if (!(await interceptFetch(value))) {
        return;
      }
    }
    searchParams.value = {
      [searchName]: value,
    };
  }
  function emitChange() {
    emit('options-change', unref(getOptions));
  }
src/components/Loading/src/Loading.vue
@@ -54,7 +54,7 @@
    justify-content: center;
    width: 100%;
    height: 100%;
    background-color: rgb(240 242 245 / 40%);
    background-color: #f0f2f566;
    &.absolute {
      position: absolute;
src/components/Preview/src/functional.ts
@@ -1,7 +1,7 @@
import type { Options, Props } from './typing';
import ImgPreview from './Functional.vue';
import { isClient } from '@/utils/is';
import { createVNode, render } from 'vue';
import ImgPreview from './Functional.vue';
import type { Options, Props } from './typing';
let instance: ReturnType<typeof createVNode> | null = null;
export function createImgPreview(options: Options) {
@@ -10,8 +10,13 @@
  const container = document.createElement('div');
  Object.assign(propsData, { show: true, index: 0, scaleStep: 100 }, options);
  instance = createVNode(ImgPreview, propsData);
  render(instance, container);
  document.body.appendChild(container);
  if (instance?.component) {
    // 存在实例时,更新props
    Object.assign(instance.component.props, propsData);
  } else {
    instance = createVNode(ImgPreview, propsData);
    render(instance, container);
    document.body.appendChild(container);
  }
  return instance.component?.exposed;
}
src/components/Scrollbar/src/Scrollbar.vue
@@ -153,11 +153,11 @@
      height: 0;
      transition: 0.3s background-color;
      border-radius: inherit;
      background-color: rgb(144 147 153 / 30%);
      background-color: #9093994d;
      cursor: pointer;
      &:hover {
        background-color: rgb(144 147 153 / 50%);
        background-color: #90939980;
      }
    }
src/components/Table/src/components/editable/EditableCell.vue
@@ -17,6 +17,7 @@
  import { treeToList } from '@/utils/helper/treeHelper';
  import { Spin } from 'ant-design-vue';
  import { parseRowKey } from '../../helper';
  import { warn } from '@/utils/log';
  export default defineComponent({
    name: 'EditableCell',
@@ -282,6 +283,7 @@
              });
            } catch (e) {
              result = false;
              warn(e);
            } finally {
              spinning.value = false;
            }
src/components/Upload/src/components/UploadModal.vue
@@ -57,7 +57,7 @@
  import { useMessage } from '@/hooks/web/useMessage';
  //   types
  import { FileItem, UploadResultStatus } from '../types/typing';
  import { basicProps } from '../props';
  import { handleFnKey, basicProps } from '../props';
  import { createTableColumns, createActionColumn } from './data';
  // utils
  import { checkImgType, getBase64WithFile } from '../helper';
@@ -161,13 +161,13 @@
  }
  // 删除
  function handleRemove(record: FileItem) {
    const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
    index !== -1 && fileListRef.value.splice(index, 1);
    isUploadingRef.value = fileListRef.value.some(
      (item) => item.status === UploadResultStatus.UPLOADING,
    );
    emit('delete', record);
  function handleRemove(obj: Record<handleFnKey, any>) {
    let { record = {}, uidKey = 'uid' } = obj;
    const index = fileListRef.value.findIndex((item) => item[uidKey] === record[uidKey]);
    if (index !== -1) {
      const removed = fileListRef.value.splice(index, 1);
      emit('delete', removed[0][uidKey]);
    }
  }
  async function uploadApiByItem(item: FileItem) {
src/design/public.less
@@ -17,15 +17,15 @@
// }
::-webkit-scrollbar-track {
  background-color: rgb(0 0 0 / 5%);
  background-color: #0000000d;
}
::-webkit-scrollbar-thumb {
  // background-color: rgba(144, 147, 153, 0.3);
  border-radius: 2px;
  // background: rgba(0, 0, 0, 0.6);
  background-color: rgb(144 147 153 / 30%);
  box-shadow: inset 0 0 6px rgb(0 0 0 / 20%);
  background-color: #9093994d;
  box-shadow: inset 0 0 6px #00000033;
}
::-webkit-scrollbar-thumb:hover {
src/hooks/web/useECharts.ts
@@ -1,8 +1,8 @@
import type { EChartsOption } from 'echarts';
import type { Ref } from 'vue';
import { computed, nextTick, ref, unref, watch } from 'vue';
import { useTimeoutFn } from '@vben/hooks';
import { tryOnUnmounted, useDebounceFn } from '@vueuse/core';
import { unref, nextTick, watch, computed, ref } from 'vue';
import { useEventListener } from '@/hooks/event/useEventListener';
import { useBreakpoint } from '@/hooks/event/useBreakpoint';
import echarts from '@/utils/lib/echarts';
@@ -49,6 +49,10 @@
      listener: resizeFn,
    });
    removeResizeFn = removeEvent;
    const resizeObserver = new ResizeObserver(resizeFn);
    resizeObserver.observe(el);
    const { widthRef, screenEnum } = useBreakpoint();
    if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
      useTimeoutFn(() => {
@@ -64,7 +68,7 @@
        useTimeoutFn(() => {
          setOptions(unref(getOptions));
          resolve(null);
        }, 30);
        }, 50);
      }
      nextTick(() => {
        useTimeoutFn(() => {
src/layouts/default/header/components/UpgradePrompt.vue
New file
@@ -0,0 +1,33 @@
<script setup lang="ts">
  import { h } from 'vue';
  import { Modal } from 'ant-design-vue';
  import { useI18n } from '@/hooks/web/useI18n';
  const { t } = useI18n();
  const localKey = 'vben-v5.0.0-upgrade-prompt';
  if (!localStorage.getItem(localKey)) {
    Modal.confirm({
      title: t('layout.header.upgrade-prompt.title'),
      content: h('div', {}, [h('p', t('layout.header.upgrade-prompt.content'))]),
      onOk() {
        handleClick();
      },
      okText: t('layout.header.upgrade-prompt.ok-text'),
      cancelText: t('common.closeText'),
    });
  }
  localStorage.setItem(localKey, String(Date.now()));
  function handleClick() {
    window.open('https://www.vben.pro', '_blank');
  }
</script>
<template>
  <div>
    <a-button type="primary" @click="handleClick">{{
      t('layout.header.upgrade-prompt.ok-text')
    }}</a-button>
  </div>
</template>
src/layouts/default/header/index.vue
@@ -33,6 +33,8 @@
    <!-- action  -->
    <div :class="`${prefixCls}-action`">
      <UpgradePrompt class="mr-2" />
      <AppSearch v-if="getShowSearch" :class="`${prefixCls}-action__item `" />
      <ErrorAction v-if="getUseErrorHandle" :class="`${prefixCls}-action__item error-action`" />
@@ -70,6 +72,7 @@
  import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
  import { propTypes } from '@/utils/propTypes';
  import UpgradePrompt from './components/UpgradePrompt.vue';
  import LayoutMenu from '../menu/index.vue';
  import LayoutTrigger from '../trigger/index.vue';
  import { ErrorAction, FullScreen, LayoutBreadcrumb, Notify, UserDropDown } from './components';
src/locales/lang/en/layout.json
@@ -15,7 +15,12 @@
    "lockScreenPassword": "Lock screen password",
    "lockScreen": "Lock screen",
    "lockScreenBtn": "Locking",
    "home": "Home"
    "home": "Home",
    "upgrade-prompt": {
      "title": "New version released",
      "content": "Vben Admin v5.0.0 preview version has been released",
      "ok-text": "Go to new version"
    }
  },
  "multipleTab": {
    "reload": "Refresh current",
src/locales/lang/zh-CN/layout.json
@@ -15,7 +15,12 @@
    "lockScreenPassword": "锁屏密码",
    "lockScreen": "锁定屏幕",
    "lockScreenBtn": "锁定",
    "home": "首页"
    "home": "首页",
    "upgrade-prompt": {
      "title": "新版本发布",
      "content": "Vben Admin v5.0.0 预览版本已发布",
      "ok-text": "前往体验新版"
    }
  },
  "multipleTab": {
    "reload": "重新加载",
src/logics/initAppConfig.ts
@@ -24,7 +24,7 @@
export function initAppConfigStore() {
  const localeStore = useLocaleStore();
  const appStore = useAppStore();
  let projCfg: ProjectConfig = Persistent.getLocal(PROJ_CFG_KEY) as ProjectConfig;
  let projCfg = Persistent.getLocal<ProjectConfig>(PROJ_CFG_KEY);
  projCfg = deepMerge(projectSetting, projCfg || {});
  const darkMode = appStore.getDarkMode;
  const {
src/store/modules/multipleTab.ts
@@ -160,7 +160,7 @@
          const realPath = meta?.realPath ?? '';
          // 获取到已经打开的动态路由数, 判断是否大于某一个值
          if (
            this.tabList.filter((e) => e.meta?.realPath ?? '' === realPath).length >= dynamicLevel
            this.tabList.filter((e) => (e.meta?.realPath ?? '') === realPath).length >= dynamicLevel
          ) {
            // 关闭第一个
            const index = this.tabList.findIndex((item) => item.meta.realPath === realPath);
src/views/demo/form/index.vue
@@ -57,8 +57,8 @@
</template>
<script lang="ts" setup>
  import { type Recordable } from '@vben/types';
  import { computed, unref, ref } from 'vue';
  import { BasicForm, ApiSelect, FormSchema } from '@/components/Form';
  import { computed, ref, unref } from 'vue';
  import { ApiSelect, BasicForm, FormSchema } from '@/components/Form';
  import { CollapseContainer } from '@/components/Container';
  import { useMessage } from '@/hooks/web/useMessage';
  import { PageWrapper } from '@/components/Page';
@@ -472,7 +472,7 @@
      },
    },
    {
      field: 'field32',
      field: 'field32-1',
      label: '下拉远程搜索',
      helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'],
      required: true,
@@ -483,6 +483,35 @@
      defaultValue: '0',
    },
    {
      field: 'field32-2',
      label: '下拉远程搜索',
      component: 'ApiSelect',
      helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'],
      componentProps: {
        api: optionsListApi,
        showSearch: true,
        apiSearch: {
          show: true,
          searchName: 'name',
        },
        resultField: 'list',
        labelField: 'name',
        valueField: 'id',
        immediate: true,
        onChange: (e, v) => {
          console.log('ApiSelect====>:', e, v);
        },
        onOptionsChange: (options) => {
          console.log('get options', options.length, options);
        },
      },
      required: true,
      colProps: {
        span: 8,
      },
      defaultValue: '0',
    },
    {
      field: 'field33',
      component: 'ApiTreeSelect',
      label: '远程下拉树',