瀏覽代碼

Merge remote-tracking branch 'upstream/main'

laiqi 1 年之前
父節點
當前提交
70edb31e89

+ 2 - 0
.gitignore

@@ -58,3 +58,5 @@ web-antd
 web-naive
 docs
 playground
+README.ja-JP.md
+README.zh-CN.md

+ 117 - 0
apps/web-ele/src/views/demos/element/index.vue

@@ -0,0 +1,117 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import {
+  ElButton,
+  ElCard,
+  ElMessage,
+  ElNotification,
+  ElSegmented,
+  ElSpace,
+  ElTable,
+} from 'element-plus';
+
+type NotificationType = 'error' | 'info' | 'success' | 'warning';
+
+function info() {
+  ElMessage.info('How many roads must a man walk down');
+}
+
+function error() {
+  ElMessage.error({
+    duration: 2500,
+    message: 'Once upon a time you dressed so fine',
+  });
+}
+
+function warning() {
+  ElMessage.warning('How many roads must a man walk down');
+}
+function success() {
+  ElMessage.success(
+    'Cause you walked hand in hand With another man in my place',
+  );
+}
+
+function notify(type: NotificationType) {
+  ElNotification({
+    duration: 2500,
+    message: '说点啥呢',
+    type,
+  });
+}
+const tableData = [
+  { prop1: '1', prop2: 'A' },
+  { prop1: '2', prop2: 'B' },
+  { prop1: '3', prop2: 'C' },
+  { prop1: '4', prop2: 'D' },
+  { prop1: '5', prop2: 'E' },
+  { prop1: '6', prop2: 'F' },
+];
+
+const segmentedValue = ref('Mon');
+
+const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+</script>
+
+<template>
+  <Page
+    description="支持多语言,主题功能集成切换等"
+    title="Element Plus组件使用演示"
+  >
+    <div class="flex flex-wrap gap-5">
+      <ElCard class="mb-5 w-auto">
+        <template #header> 按钮 </template>
+        <ElSpace>
+          <ElButton text>Text</ElButton>
+          <ElButton>Default</ElButton>
+          <ElButton type="primary"> Primary </ElButton>
+          <ElButton type="info"> Info </ElButton>
+          <ElButton type="success"> Success </ElButton>
+          <ElButton type="warning"> Warning </ElButton>
+          <ElButton type="danger"> Error </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> Message </template>
+        <ElSpace>
+          <ElButton type="info" @click="info"> 信息 </ElButton>
+          <ElButton type="danger" @click="error"> 错误 </ElButton>
+          <ElButton type="warning" @click="warning"> 警告 </ElButton>
+          <ElButton type="success" @click="success"> 成功 </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> Notification </template>
+        <ElSpace>
+          <ElButton type="info" @click="notify('info')"> 信息 </ElButton>
+          <ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
+          <ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
+          <ElButton type="success" @click="notify('success')"> 成功 </ElButton>
+        </ElSpace>
+      </ElCard>
+      <ElCard class="mb-5 w-auto">
+        <template #header> Segmented </template>
+        <ElSegmented
+          v-model="segmentedValue"
+          :options="segmentedOptions"
+          size="large"
+        />
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <template #header> V-Loading </template>
+        <div class="flex size-72 items-center justify-center" v-loading="true">
+          一些演示的内容
+        </div>
+      </ElCard>
+      <ElCard class="mb-5 w-80">
+        <ElTable :data="tableData" stripe>
+          <ElTable.TableColumn label="测试列1" prop="prop1" />
+          <ElTable.TableColumn label="测试列2" prop="prop2" />
+        </ElTable>
+      </ElCard>
+    </div>
+  </Page>
+</template>

+ 181 - 0
apps/web-ele/src/views/demos/form/basic.vue

@@ -0,0 +1,181 @@
+<script lang="ts" setup>
+import { h } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
+
+import { useVbenForm } from '#/adapter/form';
+import { getAllMenusApi } from '#/api';
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    // 所有表单项
+    componentProps: {
+      class: 'w-full',
+    },
+  },
+  layout: 'horizontal',
+  // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
+  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+  handleSubmit: (values) => {
+    ElMessage.success(`表单数据:${JSON.stringify(values)}`);
+  },
+  schema: [
+    {
+      // 组件需要在 #/adapter.ts内注册,并加上类型
+      component: 'ApiSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口转options格式
+        afterFetch: (data: { name: string; path: string }[]) => {
+          return data.map((item: any) => ({
+            label: item.name,
+            value: item.path,
+          }));
+        },
+        // 菜单接口
+        api: getAllMenusApi,
+      },
+      // 字段名
+      fieldName: 'api',
+      // 界面显示的label
+      label: 'ApiSelect',
+    },
+    {
+      component: 'ApiTreeSelect',
+      // 对应组件的参数
+      componentProps: {
+        // 菜单接口
+        api: getAllMenusApi,
+        childrenField: 'children',
+        // 菜单接口转options格式
+        labelField: 'name',
+        valueField: 'path',
+      },
+      // 字段名
+      fieldName: 'apiTree',
+      // 界面显示的label
+      label: 'ApiTreeSelect',
+    },
+    {
+      component: 'Input',
+      fieldName: 'string',
+      label: 'String',
+    },
+    {
+      component: 'InputNumber',
+      fieldName: 'number',
+      label: 'Number',
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radio',
+      label: 'Radio',
+      componentProps: {
+        options: [
+          { value: 'A', label: 'A' },
+          { value: 'B', label: 'B' },
+          { value: 'C', label: 'C' },
+          { value: 'D', label: 'D' },
+          { value: 'E', label: 'E' },
+        ],
+      },
+    },
+    {
+      component: 'RadioGroup',
+      fieldName: 'radioButton',
+      label: 'RadioButton',
+      componentProps: {
+        isButton: true,
+        options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
+          value: v,
+          label: `选项${v}`,
+        })),
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbox',
+      label: 'Checkbox',
+      componentProps: {
+        options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbox1',
+      label: 'Checkbox1',
+      renderComponentContent: () => {
+        return {
+          default: () => {
+            return ['A', 'B', 'C', 'D'].map((v) =>
+              h(ElCheckbox, { label: v, value: v }),
+            );
+          },
+        };
+      },
+    },
+    {
+      component: 'CheckboxGroup',
+      fieldName: 'checkbotton',
+      label: 'CheckBotton',
+      componentProps: {
+        isButton: true,
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+        ],
+      },
+    },
+    {
+      component: 'DatePicker',
+      fieldName: 'date',
+      label: 'Date',
+    },
+    {
+      component: 'Select',
+      fieldName: 'select',
+      label: 'Select',
+      componentProps: {
+        filterable: true,
+        options: [
+          { value: 'A', label: '选项A' },
+          { value: 'B', label: '选项B' },
+          { value: 'C', label: '选项C' },
+        ],
+      },
+    },
+  ],
+});
+function setFormValues() {
+  formApi.setValues({
+    string: 'string',
+    number: 123,
+    radio: 'B',
+    radioButton: 'C',
+    checkbox: ['A', 'C'],
+    checkbotton: ['B', 'C'],
+    checkbox1: ['A', 'B'],
+    date: new Date(),
+    select: 'B',
+  });
+}
+</script>
+<template>
+  <Page
+    description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
+    title="表单演示"
+  >
+    <ElCard>
+      <template #header>
+        <div class="flex items-center">
+          <span class="flex-auto">基础表单演示</span>
+          <ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
+        </div>
+      </template>
+      <Form />
+    </ElCard>
+  </Page>
+</template>

+ 1 - 0
packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap

@@ -80,6 +80,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
     "enable": true,
     "height": 38,
     "keepAlive": true,
+    "maxCount": 0,
     "middleClickToClose": false,
     "persist": true,
     "showIcon": true,

+ 1 - 0
packages/@core/preferences/src/config.ts

@@ -80,6 +80,7 @@ const defaultPreferences: Preferences = {
     enable: true,
     height: 38,
     keepAlive: true,
+    maxCount: 0,
     middleClickToClose: false,
     persist: true,
     showIcon: true,

+ 2 - 0
packages/@core/preferences/src/types.ts

@@ -168,6 +168,8 @@ interface TabbarPreferences {
   height: number;
   /** 开启标签页缓存功能 */
   keepAlive: boolean;
+  /** 限制最大数量 */
+  maxCount: number;
   /** 是否点击中键时关闭标签 */
   middleClickToClose: boolean;
   /** 是否持久化标签 */

+ 12 - 0
packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue

@@ -5,6 +5,7 @@ import { computed } from 'vue';
 
 import { $t } from '@vben/locales';
 
+import NumberFieldItem from '../number-field-item.vue';
 import SelectItem from '../select-item.vue';
 import SwitchItem from '../switch-item.vue';
 
@@ -22,6 +23,7 @@ const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
 const tabbarStyleType = defineModel<string>('tabbarStyleType');
 const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
 const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
+const tabbarMaxCount = defineModel<number>('tabbarMaxCount');
 const tabbarMiddleClickToClose = defineModel<boolean>(
   'tabbarMiddleClickToClose',
 );
@@ -54,6 +56,16 @@ const styleItems = computed((): SelectOption[] => [
   <SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.persist') }}
   </SwitchItem>
+  <NumberFieldItem
+    v-model="tabbarMaxCount"
+    :disabled="!tabbarEnable"
+    :max="30"
+    :min="0"
+    :step="5"
+    :tip="$t('preferences.tabbar.maxCountTip')"
+  >
+    {{ $t('preferences.tabbar.maxCount') }}
+  </NumberFieldItem>
   <SwitchItem v-model="tabbarDraggable" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.draggable') }}
   </SwitchItem>

+ 10 - 2
packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue

@@ -23,10 +23,12 @@ withDefaults(
     disabled?: boolean;
     items?: SelectOption[];
     placeholder?: string;
+    tip?: string;
   }>(),
   {
     disabled: false,
     placeholder: '',
+    tip: '',
     items: () => [],
   },
 );
@@ -47,11 +49,17 @@ const slots = useSlots();
     <span class="flex items-center text-sm">
       <slot></slot>
 
-      <VbenTooltip v-if="slots.tip" side="bottom">
+      <VbenTooltip v-if="slots.tip || tip" side="bottom">
         <template #trigger>
           <CircleHelp class="ml-1 size-3 cursor-help" />
         </template>
-        <slot name="tip"></slot>
+        <slot name="tip">
+          <template v-if="tip">
+            <p v-for="(line, index) in tip.split('\n')" :key="index">
+              {{ line }}
+            </p>
+          </template>
+        </slot>
       </VbenTooltip>
     </span>
 

+ 2 - 0
packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue

@@ -116,6 +116,7 @@ const tabbarPersist = defineModel<boolean>('tabbarPersist');
 const tabbarDraggable = defineModel<boolean>('tabbarDraggable');
 const tabbarWheelable = defineModel<boolean>('tabbarWheelable');
 const tabbarStyleType = defineModel<string>('tabbarStyleType');
+const tabbarMaxCount = defineModel<number>('tabbarMaxCount');
 const tabbarMiddleClickToClose = defineModel<boolean>(
   'tabbarMiddleClickToClose',
 );
@@ -365,6 +366,7 @@ async function handleReset() {
                 v-model:tabbar-show-more="tabbarShowMore"
                 v-model:tabbar-style-type="tabbarStyleType"
                 v-model:tabbar-wheelable="tabbarWheelable"
+                v-model:tabbar-max-count="tabbarMaxCount"
                 v-model:tabbar-middle-click-to-close="tabbarMiddleClickToClose"
               />
             </Block>

+ 2 - 0
packages/locales/src/langs/en-US/preferences.json

@@ -62,6 +62,8 @@
     "showMore": "Show More Button",
     "showMaximize": "Show Maximize Button",
     "persist": "Persist Tabs",
+    "maxCount": "Max Count of Tabs",
+    "maxCountTip": "When the number of tabs exceeds the maximum,\nthe oldest tab will be closed.\n Set to 0 to disable count checking.",
     "draggable": "Enable Draggable Sort",
     "wheelable": "Support Mouse Wheel",
     "middleClickClose": "Close Tab when Mouse Middle Button Click",

+ 2 - 0
packages/locales/src/langs/zh-CN/preferences.json

@@ -62,6 +62,8 @@
     "showMore": "显示更多按钮",
     "showMaximize": "显示最大化按钮",
     "persist": "持久化标签页",
+    "maxCount": "最大标签数",
+    "maxCountTip": "每次打开新的标签时如果超过最大标签数,\n会自动关闭一个最先打开的标签\n设置为 0 则不限制",
     "draggable": "启动拖拽排序",
     "wheelable": "启用纵向滚轮响应",
     "middleClickClose": "点击鼠标中键关闭标签页",

+ 1 - 0
packages/stores/package.json

@@ -20,6 +20,7 @@
     }
   },
   "dependencies": {
+    "@vben-core/preferences": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben-core/typings": "workspace:*",
     "pinia": "catalog:",

+ 9 - 1
packages/stores/src/modules/tabbar.ts

@@ -4,6 +4,7 @@ import type { TabDefinition } from '@vben-core/typings';
 
 import { toRaw } from 'vue';
 
+import { preferences } from '@vben-core/preferences';
 import {
   openRouteInNewWindow,
   startProgress,
@@ -107,6 +108,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
       });
 
       if (tabIndex === -1) {
+        const maxCount = preferences.tabbar.maxCount;
         // 获取动态路由打开数,超过 0 即代表需要控制打开数
         const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ??
           -1) as number;
@@ -122,8 +124,14 @@ export const useTabbarStore = defineStore('core-tabbar', {
             (item) => item.name === routeTab.name,
           );
           index !== -1 && this.tabs.splice(index, 1);
+        } else if (maxCount > 0 && this.tabs.length >= maxCount) {
+          // 关闭第一个
+          const index = this.tabs.findIndex(
+            (item) =>
+              !Reflect.has(item.meta, 'affixTab') || !item.meta.affixTab,
+          );
+          index !== -1 && this.tabs.splice(index, 1);
         }
-
         this.tabs.push(tab);
       } else {
         // 页面已经存在,不重复添加选项卡,只更新选项卡参数

File diff suppressed because it is too large
+ 576 - 69
pnpm-lock.yaml


+ 1 - 1
pnpm-workspace.yaml

@@ -75,7 +75,7 @@ catalog:
   commitlint-plugin-function-rules: ^4.0.1
   consola: ^3.4.0
   cross-env: ^7.0.3
-  cspell: ^8.17.2
+  cspell: ^8.17.3
   cssnano: ^7.0.6
   cz-git: ^1.11.0
   czg: ^1.11.0

+ 20 - 0
vben-admin.code-workspace

@@ -1,10 +1,26 @@
 {
   "folders": [
     {
+      "name": "@vben/backend-mock",
+      "path": "apps/backend-mock",
+    },
+    {
+      "name": "@vben/web-antd",
+      "path": "apps/web-antd",
+    },
+    {
       "name": "@vben/web-ele",
       "path": "apps/web-ele",
     },
     {
+      "name": "@vben/web-naive",
+      "path": "apps/web-naive",
+    },
+    {
+      "name": "@vben/docs",
+      "path": "docs",
+    },
+    {
       "name": "@vben/commitlint-config",
       "path": "internal/lint-configs/commitlint-config",
     },
@@ -141,6 +157,10 @@
       "path": "packages/utils",
     },
     {
+      "name": "@vben/playground",
+      "path": "playground",
+    },
+    {
       "name": "@vben/turbo-run",
       "path": "scripts/turbo-run",
     },

Some files were not shown because too many files changed in this diff