Merge remote-tracking branch 'origin/v2' into onbus-crm
| | |
| | | |
| | | PATH="/usr/local/bin:$PATH" |
| | | |
| | | npx --no-install commitlint --edit "$1" |
| | | # npx --no-install commitlint --edit "$1" |
| | |
| | | PATH="/usr/local/bin:$PATH" |
| | | |
| | | # Format and submit code according to lintstagedrc.js configuration |
| | | pnpm exec lint-staged |
| | | # pnpm exec lint-staged |
| | |
| | | @dropdown-visible-change="handleFetch" |
| | | v-bind="$attrs" |
| | | @change="handleChange" |
| | | @search="debounceSearchFn" |
| | | :options="getOptions" |
| | | v-model:value="state" |
| | | > |
| | |
| | | </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 }); |
| | | |
| | |
| | | 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 |
| | |
| | | options: { |
| | | type: Array<OptionsItem>, |
| | | default: [], |
| | | }, |
| | | apiSearch: { |
| | | type: Object as PropType<ApiSearchOption>, |
| | | default: () => null, |
| | | }, |
| | | beforeFetch: { |
| | | type: Function as PropType<Fn>, |
| | |
| | | // 首次是否加载过了 |
| | | 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 |
| | |
| | | { 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; |
| | | } |
| | |
| | | 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)); |
| | | } |
| | |
| | | justify-content: center; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: rgb(240 242 245 / 40%); |
| | | background-color: #f0f2f566; |
| | | |
| | | &.absolute { |
| | | position: absolute; |
| | |
| | | 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) { |
| | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | 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', |
| | |
| | | }); |
| | | } catch (e) { |
| | | result = false; |
| | | warn(e); |
| | | } finally { |
| | | spinning.value = false; |
| | | } |
| | |
| | | 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'; |
| | |
| | | } |
| | | |
| | | // 删除 |
| | | 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) { |
| | |
| | | // } |
| | | |
| | | ::-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 { |
| | |
| | | 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'; |
| | |
| | | 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(() => { |
| | |
| | | useTimeoutFn(() => { |
| | | setOptions(unref(getOptions)); |
| | | resolve(null); |
| | | }, 30); |
| | | }, 50); |
| | | } |
| | | nextTick(() => { |
| | | useTimeoutFn(() => { |
New file |
| | |
| | | <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> |
| | |
| | | |
| | | <!-- 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`" /> |
| | |
| | | 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'; |
| | |
| | | "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", |
| | |
| | | "lockScreenPassword": "锁屏密码", |
| | | "lockScreen": "锁定屏幕", |
| | | "lockScreenBtn": "锁定", |
| | | "home": "首页" |
| | | "home": "首页", |
| | | "upgrade-prompt": { |
| | | "title": "新版本发布", |
| | | "content": "Vben Admin v5.0.0 预览版本已发布", |
| | | "ok-text": "前往体验新版" |
| | | } |
| | | }, |
| | | "multipleTab": { |
| | | "reload": "重新加载", |
| | |
| | | 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 { |
| | |
| | | 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); |
| | |
| | | </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'; |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'field32', |
| | | field: 'field32-1', |
| | | label: '下拉远程搜索', |
| | | helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'], |
| | | required: true, |
| | |
| | | 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: '远程下拉树', |