Sfoglia il codice sorgente

feat(form-page): add form page demo

vben 4 anni fa
parent
commit
0b6110a8fc
43 ha cambiato i file con 1389 aggiunte e 116 eliminazioni
  1. 3 0
      CHANGELOG.zh_CN.md
  2. 2 0
      README.md
  3. 1 1
      package.json
  4. 5 1
      src/components/Basic/src/BasicHelp.vue
  5. 1 0
      src/components/Footer/index.ts
  6. 57 0
      src/components/Footer/src/index.vue
  7. 22 7
      src/components/Form/src/BasicForm.vue
  8. 12 4
      src/components/Form/src/FormItem.tsx
  9. 1 1
      src/components/Form/src/helper.ts
  10. 11 4
      src/components/Form/src/hooks/useLabelWidth.ts
  11. 3 0
      src/components/Form/src/types/form.ts
  12. 1 2
      src/components/Menu/src/BasicMenu.tsx
  13. 3 1
      src/components/Table/index.ts
  14. 2 2
      src/components/Table/src/components/TableAction.tsx
  15. 99 30
      src/components/Table/src/components/renderEditable.tsx
  16. 4 4
      src/components/Table/src/types/table.ts
  17. 0 1
      src/components/Table/src/types/tableAction.ts
  18. 6 1
      src/components/registerGlobComp.ts
  19. 7 4
      src/design/ant/index.less
  20. 8 0
      src/design/index.less
  21. 2 2
      src/design/transition/base.less
  22. 1 1
      src/design/transition/fade.less
  23. 1 1
      src/design/var/index.less
  24. 1 1
      src/layouts/default/menu/LayoutMenu.tsx
  25. 4 4
      src/router/menus/modules/demo/form.ts
  26. 21 0
      src/router/menus/modules/demo/page.ts
  27. 10 0
      src/router/menus/modules/demo/table.ts
  28. 37 0
      src/router/routes/modules/demo/page.ts
  29. 8 0
      src/router/routes/modules/demo/table.ts
  30. 0 1
      src/views/demo/page/account/center/Project.vue
  31. 119 0
      src/views/demo/page/form/basic/data.ts
  32. 66 0
      src/views/demo/page/form/basic/index.vue
  33. 142 0
      src/views/demo/page/form/high/PersonTable.vue
  34. 149 0
      src/views/demo/page/form/high/data.ts
  35. 72 0
      src/views/demo/page/form/high/index.vue
  36. 95 0
      src/views/demo/page/form/step/Step1.vue
  37. 71 0
      src/views/demo/page/form/step/Step2.vue
  38. 45 0
      src/views/demo/page/form/step/Step3.vue
  39. 63 0
      src/views/demo/page/form/step/data.tsx
  40. 86 0
      src/views/demo/page/form/step/index.vue
  41. 105 0
      src/views/demo/table/EditRowTable.vue
  42. 7 7
      src/views/demo/table/FixedColumn.vue
  43. 36 36
      yarn.lock

+ 3 - 0
CHANGELOG.zh_CN.md

@@ -4,6 +4,9 @@
 
 - 新增 base64 文件流下载
 - 优化上传组件及示例
+- 新增可编辑行示例
+- 新增个人页
+- 新增表单页
 
 ### 🎫 Chores
 

+ 2 - 0
README.md

@@ -232,6 +232,8 @@ yarn clean:lib # 删除node_modules,兼容window系统
 
 ## 正在开发的功能
 
+- [ ] 新分支全局国家化
+- [ ] 示例 page 页面
 - [ ] 主题配置
 - [ ] 黑暗主题
 - [ ] 打包 CDN

+ 1 - 1
package.json

@@ -23,7 +23,7 @@
   "dependencies": {
     "@iconify/iconify": "^2.0.0-rc.2",
     "@vueuse/core": "^4.0.0-beta.41",
-    "ant-design-vue": "^2.0.0-rc.1",
+    "ant-design-vue": "^2.0.0-beta.15",
     "apexcharts": "3.22.0",
     "axios": "^0.21.0",
     "echarts": "^4.9.0",

+ 5 - 1
src/components/Basic/src/BasicHelp.vue

@@ -48,6 +48,10 @@
           bottom: 0,
         }),
       },
+      placement: {
+        type: String as PropType<string>,
+        defualt: 'right',
+      },
     },
     setup(props, { slots }) {
       const getOverlayStyleRef = computed(() => {
@@ -97,7 +101,7 @@
             overlayClassName: 'base-help__wrap',
             autoAdjustOverflow: true,
             overlayStyle: unref(getOverlayStyleRef),
-            placement: 'right',
+            placement: props.placement,
             getPopupContainer: () => getPopupContainer(),
           },
           {

+ 1 - 0
src/components/Footer/index.ts

@@ -0,0 +1 @@
+export { default as AppFooter } from './src/index.vue';

+ 57 - 0
src/components/Footer/src/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="app-footer" :style="{ width: getWidth }">
+    <div class="app-footer__left">
+      <slot name="left" />
+    </div>
+    <div class="app-footer__right">
+      <slot name="right" />
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, computed, unref } from 'vue';
+  import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
+  import { appStore } from '/@/store/modules/app';
+  import { menuStore } from '/@/store/modules/menu';
+  export default defineComponent({
+    name: 'AppFooter',
+    setup() {
+      const getMiniWidth = computed(() => {
+        const {
+          menuSetting: { collapsedShowTitle },
+        } = appStore.getProjectConfig;
+        return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH;
+      });
+
+      const getWidth = computed(() => {
+        const { getCollapsedState, getMenuWidthState } = menuStore;
+        const width = getCollapsedState ? unref(getMiniWidth) : getMenuWidthState;
+        return `calc(100% - ${width}px)`;
+      });
+
+      return { getWidth };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .app-footer {
+    position: fixed;
+    right: 0;
+    bottom: 0;
+    z-index: 99;
+    display: flex;
+    width: 100%;
+    align-items: center;
+    padding: 0 24px;
+    line-height: 44px;
+    background: #fff;
+    border-top: 1px solid #f0f0f0;
+    box-shadow: 0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),
+      0 -12px 48px 16px rgba(0, 0, 0, 0.03);
+    transition: width 0.3s;
+
+    &__left {
+      flex: 1 1;
+    }
+  }
+</style>

+ 22 - 7
src/components/Form/src/BasicForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Form v-bind="$attrs" ref="formElRef" :model="formModel">
+  <Form v-bind="{ ...$attrs, ...$props }" ref="formElRef" :model="formModel">
     <Row :class="getProps.compact ? 'compact-form-row' : ''">
       <slot name="formHeader" />
       <template v-for="schema in getSchema" :key="schema.field">
@@ -54,8 +54,8 @@
       const formModel = reactive({});
 
       const actionState = reactive({
-        resetAction: () => {},
-        submitAction: () => {},
+        resetAction: {},
+        submitAction: {},
       });
 
       const advanceState = reactive<AdvanceState>({
@@ -67,7 +67,7 @@
 
       const defaultValueRef = ref<any>({});
       const propsRef = ref<Partial<FormProps>>({});
-      const schemaRef = ref<FormSchema[] | null>(null);
+      const schemaRef = ref<Nullable<FormSchema[]>>(null);
       const formElRef = ref<Nullable<FormActionType>>(null);
 
       const getMergePropsRef = computed(
@@ -98,7 +98,15 @@
         for (const schema of schemas) {
           const { defaultValue, component } = schema;
           if (defaultValue && dateItemType.includes(component!)) {
-            schema.defaultValue = moment(defaultValue);
+            if (!Array.isArray(defaultValue)) {
+              schema.defaultValue = moment(defaultValue);
+            } else {
+              const def: moment.Moment[] = [];
+              defaultValue.forEach((item) => {
+                def.push(moment(item));
+              });
+              schema.defaultValue = def;
+            }
           }
         }
         return schemas as FormSchema[];
@@ -139,8 +147,8 @@
         formModel,
         getSchema,
         defaultValueRef,
-        formElRef: formElRef as any,
-        schemaRef: schemaRef as any,
+        formElRef: formElRef as Ref<FormActionType>,
+        schemaRef: schemaRef as Ref<FormSchema[]>,
         handleFormValues,
         actionState,
       });
@@ -156,6 +164,13 @@
         }
       );
 
+      watch(
+        () => getSchema.value,
+        () => {
+          initDefault();
+        }
+      );
+
       /**
        * @description:设置表单
        */

+ 12 - 4
src/components/Form/src/FormItem.tsx

@@ -250,14 +250,21 @@ export default defineComponent({
     }
 
     function renderLabelHelpMessage() {
-      const { label, helpMessage, helpComponentProps } = props.schema;
+      const { label, helpMessage, helpComponentProps, subLabel } = props.schema;
+      const renderLabel = subLabel ? (
+        <span>
+          {label} <span style="color:#00000073">{subLabel}</span>
+        </span>
+      ) : (
+        label
+      );
       if (!helpMessage || (Array.isArray(helpMessage) && helpMessage.length === 0)) {
-        return label;
+        return renderLabel;
       }
       return (
         <span>
-          {label}
-          <BasicHelp class="mx-1" text={helpMessage} {...helpComponentProps} />
+          {renderLabel}
+          <BasicHelp placement="top" class="mx-1" text={helpMessage} {...helpComponentProps} />
         </span>
       );
     }
@@ -291,6 +298,7 @@ export default defineComponent({
       const { colProps = {}, colSlot, renderColContent, component } = props.schema;
       if (!componentMap.has(component)) return null;
       const { baseColProps = {} } = props.formProps;
+
       const realColProps = { ...baseColProps, ...colProps };
       const { isIfShow, isShow } = getShow();
       const getContent = () => {

+ 1 - 1
src/components/Form/src/helper.ts

@@ -7,7 +7,7 @@ export function createPlaceholderMessage(component: ComponentType) {
   if (component.includes('Input') || component.includes('Complete')) {
     return '请输入';
   }
-  if (component.includes('Picker') && !component.includes('Range')) {
+  if (component.includes('Picker')) {
     return '请选择';
   }
   if (

+ 11 - 4
src/components/Form/src/hooks/useLabelWidth.ts

@@ -25,19 +25,26 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
     const { labelCol = {}, wrapperCol = {} } = schemaItem.itemProps || {};
     const { labelWidth, disabledLabelWidth } = schemaItem;
 
-    const { labelWidth: globalLabelWidth } = unref(propsRef) as any;
+    const {
+      labelWidth: globalLabelWidth,
+      labelCol: globalLabelCol,
+      wrapperCol: globWrapperCol,
+    } = unref(propsRef) as any;
+
     // 如果全局有设置labelWidth, 则所有item使用
-    if ((!globalLabelWidth && !labelWidth) || disabledLabelWidth) {
+    if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) {
       return { labelCol, wrapperCol };
     }
     let width = labelWidth || globalLabelWidth;
+    const col = { ...globalLabelCol, ...labelCol };
+    const wrapCol = { ...globWrapperCol, ...wrapperCol };
 
     if (width) {
       width = isNumber(width) ? `${width}px` : width;
     }
     return {
-      labelCol: { style: { width }, ...labelCol },
-      wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapperCol },
+      labelCol: { style: { width }, ...col },
+      wrapperCol: { style: { width: `calc(100% - ${width})` }, ...wrapCol },
     };
   });
 }

+ 3 - 0
src/components/Form/src/types/form.ts

@@ -41,6 +41,7 @@ export type RegisterFn = (formInstance: FormActionType) => void;
 export type UseFormReturnType = [RegisterFn, FormActionType];
 
 export interface FormProps {
+  // layout?: 'vertical' | 'inline' | 'horizontal';
   // 表单值
   model?: any;
   // 整个表单所有项宽度
@@ -108,6 +109,8 @@ export interface FormSchema {
   valueField?: string;
   // 标签名
   label: string;
+  // 辅助文本
+  subLabel?: string;
   // 文本右侧帮助文本
   helpMessage?: string | string[];
   // BaseHelp组件props

+ 1 - 2
src/components/Menu/src/BasicMenu.tsx

@@ -136,9 +136,9 @@ export default defineComponent({
         const flag = await beforeClickFn(menu);
         if (!flag) return;
       }
+      emit('menuClick', menu);
       const { path } = menu;
       menuState.selectedKeys = [path];
-      emit('menuClick', menu);
     }
 
     function handleMenuChange() {
@@ -219,7 +219,6 @@ export default defineComponent({
         : {};
       return (
         <Menu
-          forceSubMenuRender={props.isAppMenu}
           selectedKeys={selectedKeys}
           defaultSelectedKeys={defaultSelectedKeys}
           mode={mode}

+ 3 - 1
src/components/Table/index.ts

@@ -1,7 +1,7 @@
 export { default as BasicTable } from './src/BasicTable.vue';
 export { default as TableAction } from './src/components/TableAction';
 export { default as TableImg } from './src/components/TableImg.vue';
-export { renderEditableCell } from './src/components/renderEditableCell';
+export { renderEditableCell, renderEditableRow } from './src/components/renderEditable';
 export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
 
 export * from './src/types/table';
@@ -11,3 +11,5 @@ export * from './src/types/tableAction';
 export { useTable } from './src/hooks/useTable';
 
 export type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
+
+export type { EditRecordRow } from './src/components/renderEditable';

+ 2 - 2
src/components/Table/src/components/TableAction.tsx

@@ -32,7 +32,7 @@ export default defineComponent({
           disabled={disabled}
           color={color}
           {...action}
-          key={index}
+          key={`${index}-${label}`}
         >
           {() => (
             <>
@@ -60,7 +60,7 @@ export default defineComponent({
       } = popConfirm;
       return (
         <Popconfirm
-          key={`P-${index}`}
+          key={`p-${index}-${title}`}
           title={title}
           onConfirm={confirm}
           onCancel={cancel}

+ 99 - 30
src/components/Table/src/components/renderEditableCell.tsx → src/components/Table/src/components/renderEditable.tsx

@@ -1,15 +1,15 @@
-import { defineComponent, PropType, ref, unref, nextTick } from 'vue';
-import { injectTable } from '../hooks/useProvinceTable';
+import { defineComponent, PropType, ref, unref, nextTick, watchEffect } from 'vue';
 import ClickOutSide from '/@/components/ClickOutSide/index.vue';
 
 import { RenderEditableCellParams } from '../types/table';
 import { ComponentType } from '../types/componentType';
 
 import { componentMap } from '../componentMap';
-import '../style/editable-cell.less';
 import { isString, isBoolean } from '/@/utils/is';
 import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
 
+import '../style/editable-cell.less';
+
 const prefixCls = 'editable-cell';
 const EditableCell = defineComponent({
   name: 'EditableCell',
@@ -37,14 +37,35 @@ const EditableCell = defineComponent({
       type: String as PropType<ComponentType>,
       default: 'Input',
     },
+    editable: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+    editRow: {
+      type: Boolean as PropType<boolean>,
+      default: false,
+    },
+    record: {
+      type: Object as PropType<EditRecordRow>,
+    },
+    placeholder: {
+      type: String as PropType<string>,
+    },
   },
   emits: ['submit', 'cancel'],
   setup(props, { attrs, emit }) {
-    const table = injectTable();
     const elRef = ref<any>(null);
 
     const isEditRef = ref(false);
     const currentValueRef = ref<string | boolean>(props.value);
+    const defaultValueRef = ref<string | boolean>(props.value);
+
+    watchEffect(() => {
+      defaultValueRef.value = props.value;
+      if (isBoolean(props.editable)) {
+        isEditRef.value = props.editable;
+      }
+    });
 
     function handleChange(e: any) {
       if (e && e.target && Reflect.has(e.target, 'value')) {
@@ -65,37 +86,55 @@ const EditableCell = defineComponent({
 
     function handleCancel() {
       isEditRef.value = false;
+      currentValueRef.value = defaultValueRef.value;
       emit('cancel');
     }
 
+    if (props.record) {
+      /* eslint-disable  */
+      props.record.onCancel = handleCancel;
+      /* eslint-disable */
+      props.record.onSubmit = handleSubmit;
+    }
+
     function handleSubmit() {
       const { dataKey, dataIndex } = props;
-      if (!dataKey || !dataIndex) {
-        return;
-      }
-      isEditRef.value = false;
+      if (!dataKey || !dataIndex) return;
 
-      const { getDataSource } = table;
-      const dataSource = getDataSource();
-      const target = dataSource.find((item) => item.key === dataKey);
-      if (target) {
-        target[dataIndex] = unref(currentValueRef);
-        emit('submit', { dataKey, dataIndex, value: unref(currentValueRef) });
+      if (props.record) {
+        /* eslint-disable */
+        props.record[dataIndex] = unref(currentValueRef) as string;
       }
+      isEditRef.value = false;
     }
 
     function onClickOutside() {
+      if (props.editRow) return;
       const { component } = props;
 
       if (component && component.includes('Input')) {
         handleCancel();
       }
     }
+
+    function renderValue() {
+      const { value } = props;
+      if (props.editRow) {
+        return !unref(isEditRef) ? value : null;
+      }
+      return (
+        !unref(isEditRef) && (
+          <div class={`${prefixCls}__normal`} onClick={handleEdit}>
+            {value}
+            <FormOutlined class={`${prefixCls}__normal-icon`} />
+          </div>
+        )
+      );
+    }
     return () => {
-      const { value, component, componentProps = {} } = props;
+      const { component, componentProps = {} } = props;
 
       const Comp = componentMap.get(component!) as any;
-      // const propsData: any = {};
       return (
         <div class={prefixCls}>
           {unref(isEditRef) && (
@@ -103,6 +142,7 @@ const EditableCell = defineComponent({
               {() => (
                 <div class={`${prefixCls}__wrapper`}>
                   <Comp
+                    placeholder={props.placeholder}
                     {...{
                       ...attrs,
                       ...componentProps,
@@ -114,21 +154,20 @@ const EditableCell = defineComponent({
                     onChange={handleChange}
                     onPressEnter={handleSubmit}
                   />
-                  <div class={`${prefixCls}__action`}>
-                    <CheckOutlined class={[`${prefixCls}__icon`, 'mx-2']} onClick={handleSubmit} />
-                    <CloseOutlined class={[`${prefixCls}__icon `]} onClick={handleCancel} />
-                  </div>
+                  {!props.editRow && (
+                    <div class={`${prefixCls}__action`}>
+                      <CheckOutlined
+                        class={[`${prefixCls}__icon`, 'mx-2']}
+                        onClick={handleSubmit}
+                      />
+                      <CloseOutlined class={[`${prefixCls}__icon `]} onClick={handleCancel} />
+                    </div>
+                  )}
                 </div>
               )}
             </ClickOutSide>
           )}
-
-          {!unref(isEditRef) && (
-            <div class={`${prefixCls}__normal`} onClick={handleEdit}>
-              {value}
-              <FormOutlined class={`${prefixCls}__normal-icon`} />
-            </div>
-          )}
+          {renderValue()}
         </div>
       );
     };
@@ -138,15 +177,16 @@ const EditableCell = defineComponent({
 export function renderEditableCell({
   dataIndex,
   component,
-  componentOn = {},
   componentProps = {},
+  placeholder,
 }: RenderEditableCellParams) {
-  return ({ text, record }: { text: string; record: any }) => {
+  return ({ text, record }: { text: string; record: EditRecordRow }) => {
     return (
       <EditableCell
-        {...componentOn}
         {...componentProps}
+        placeholder={placeholder}
         value={text}
+        record={record}
         dataKey={record.key}
         dataIndex={dataIndex}
         component={component}
@@ -154,3 +194,32 @@ export function renderEditableCell({
     );
   };
 }
+
+export function renderEditableRow({
+  dataIndex,
+  component,
+  componentProps = {},
+  placeholder,
+}: RenderEditableCellParams) {
+  return ({ text, record }: { text: string; record: EditRecordRow }) => {
+    return (
+      <EditableCell
+        {...componentProps}
+        value={text}
+        placeholder={placeholder}
+        editRow={true}
+        editable={record.editable}
+        dataKey={record.key}
+        record={record}
+        dataIndex={dataIndex}
+        component={component}
+      />
+    );
+  };
+}
+
+export type EditRecordRow<T = { [key: string]: any }> = {
+  editable: boolean;
+  onCancel: Fn;
+  onSubmit: Fn;
+} & T;

+ 4 - 4
src/components/Table/src/types/table.ts

@@ -68,8 +68,8 @@ export interface SorterResult {
 export interface RenderEditableCellParams {
   dataIndex: string;
   component?: ComponentType;
-  componentOn?: { [key: string]: Fn };
   componentProps?: any;
+  placeholder?: string;
 }
 
 export interface FetchParams {
@@ -88,15 +88,15 @@ export type SizeType = 'default' | 'middle' | 'small' | 'large';
 
 export interface TableActionType {
   reload: (opt?: FetchParams) => Promise<void>;
-  getSelectRows: () => any[];
+  getSelectRows: <T = any>() => T[];
   clearSelectedRowKeys: () => void;
   getSelectRowKeys: () => string[];
   deleteSelectRowByKey: (key: string) => void;
   setPagination: (info: Partial<PaginationProps>) => void;
-  setTableData: (values: any[]) => void;
+  setTableData: <T = any>(values: T[]) => void;
   getColumns: (opt?: GetColumnsParams) => BasicColumn[];
   setColumns: (columns: BasicColumn[] | string[]) => void;
-  getDataSource: () => any[];
+  getDataSource: <T = any>() => T[];
   setLoading: (loading: boolean) => void;
   setProps: (props: Partial<BasicTableProps>) => void;
   redoHeight: () => void;

+ 0 - 1
src/components/Table/src/types/tableAction.ts

@@ -1,5 +1,4 @@
 export interface ActionItem {
-  on?: any;
   onClick?: any;
   label: string;
   disabled?: boolean;

+ 6 - 1
src/components/registerGlobComp.ts

@@ -1,5 +1,6 @@
 import Icon from './Icon/index';
 import Button from './Button/index.vue';
+import { AppFooter } from './Footer';
 import {
   // Need
   Button as AntButton,
@@ -28,10 +29,12 @@ import {
   Upload,
   Transfer,
   Steps,
+  PageHeader,
+  Result,
 } from 'ant-design-vue';
 import { getApp } from '/@/useApp';
 
-const compList = [Icon, Button, AntButton.Group];
+const compList = [Icon, Button, AntButton.Group, AppFooter];
 
 // Fix hmr multiple registered components
 let registered = false;
@@ -70,5 +73,7 @@ export function registerGlobComp() {
     .use(Upload)
     .use(Transfer)
     .use(Steps)
+    .use(PageHeader)
+    .use(Result)
     .use(Tabs);
 }

+ 7 - 4
src/design/ant/index.less

@@ -109,18 +109,21 @@
   content: '';
 }
 
+.ant-form-item-with-help {
+  margin-bottom: 0;
+}
+
 .ant-form-item {
   &-label label::after {
     margin: 0 6px 0 2px;
   }
+}
 
-  &-control {
-    line-height: 36px;
-  }
+.ant-form-item:not(.ant-form-item-with-help) {
+  margin-bottom: 20px;
 }
 
 .ant-form-explain {
-  margin-bottom: 2px;
   font-size: 14px;
 }
 

+ 8 - 0
src/design/index.less

@@ -13,6 +13,14 @@
   box-sizing: border-box;
 }
 
+input:-webkit-autofill {
+  -webkit-box-shadow: 0 0 0 1000px white inset !important;
+}
+
+:-webkit-autofill {
+  transition: background-color 5000s ease-in-out 0s !important;
+}
+
 // Background color setting in full screen state in each browser
 ::backdrop,
 html,

+ 2 - 2
src/design/transition/base.less

@@ -1,11 +1,11 @@
 .transition-default() {
   &-enter-active,
   &-leave-active {
-    transition: 0.2s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
+    transition: 0.1s cubic-bezier(0.25, 0.8, 0.5, 1) !important;
   }
 
   &-move {
-    transition: transform 0.5s;
+    transition: transform 0.2s;
   }
 }
 

+ 1 - 1
src/design/transition/fade.less

@@ -11,7 +11,7 @@
 /* fade-slide */
 .fade-slide-leave-active,
 .fade-slide-enter-active {
-  transition: all 0.5s;
+  transition: all 0.2s;
 }
 
 .fade-slide-enter-from {

+ 1 - 1
src/design/var/index.less

@@ -16,4 +16,4 @@
 @page-loading-z-index: 10000;
 
 // left-menu
-@app-menu-item-height: 44px;
+@app-menu-item-height: 46px;

+ 1 - 1
src/layouts/default/menu/LayoutMenu.tsx

@@ -154,12 +154,12 @@ export default defineComponent({
     function handleMenuClick(menu: Menu) {
       const { path } = menu;
       if (path) {
+        push(path);
         const { splitType } = props;
         // split mode top
         if (splitType === MenuSplitTyeEnum.TOP) {
           menuStore.commitCurrentTopSplitMenuPathState(path);
         }
-        push(path);
       }
     }
 

+ 4 - 4
src/router/menus/modules/demo/form.ts

@@ -14,7 +14,7 @@ const menu: MenuModule = {
         name: '基础表单',
         tag: {
           type: 'warn',
-          content: 'updated',
+          dot: true,
         },
       },
       {
@@ -34,7 +34,7 @@ const menu: MenuModule = {
         name: '表单校验',
         tag: {
           type: 'warn',
-          content: 'updated',
+          dot: true,
         },
       },
       {
@@ -42,7 +42,7 @@ const menu: MenuModule = {
         name: '动态表单',
         tag: {
           type: 'warn',
-          content: 'updated',
+          dot: true,
         },
       },
       {
@@ -50,7 +50,7 @@ const menu: MenuModule = {
         name: '自定义组件',
         tag: {
           type: 'warn',
-          content: 'updated',
+          dot: true,
         },
       },
     ],

+ 21 - 0
src/router/menus/modules/demo/page.ts

@@ -16,6 +16,27 @@ const menu: MenuModule = {
     },
     children: [
       {
+        path: 'form',
+        name: '表单页',
+        tag: {
+          content: 'new',
+        },
+        children: [
+          {
+            path: 'basic',
+            name: '基础表单',
+          },
+          {
+            path: 'step',
+            name: '分步表单',
+          },
+          {
+            path: 'high',
+            name: '高级表单',
+          },
+        ],
+      },
+      {
         path: 'result',
         name: '结果页',
         tag: {

+ 10 - 0
src/router/menus/modules/demo/table.ts

@@ -4,6 +4,9 @@ const menu: MenuModule = {
   menu: {
     path: '/table',
     name: 'Table',
+    tag: {
+      dot: true,
+    },
     children: [
       {
         path: 'basic',
@@ -61,6 +64,13 @@ const menu: MenuModule = {
         path: 'editCellTable',
         name: '可编辑单元格',
       },
+      {
+        path: 'editRowTable',
+        name: '可编辑行',
+        tag: {
+          content: 'new',
+        },
+      },
     ],
   },
 };

+ 37 - 0
src/router/routes/modules/demo/page.ts

@@ -15,6 +15,43 @@ const page: AppRouteModule = {
     title: '页面',
   },
   children: [
+    // =============================form start=============================
+    {
+      path: '/form',
+      name: 'FormPage',
+      redirect: '/page-demo/form/basic',
+      meta: {
+        title: '表单页',
+      },
+      children: [
+        {
+          path: 'basic',
+          name: 'FormBasicPage',
+          component: () => import('/@/views/demo/page/form/basic/index.vue'),
+          meta: {
+            title: '基础表单',
+          },
+        },
+        {
+          path: 'step',
+          name: 'FormStepPage',
+          component: () => import('/@/views/demo/page/form/step/index.vue'),
+          meta: {
+            title: '分步表单',
+          },
+        },
+        {
+          path: 'high',
+          name: 'FormHightPage',
+          component: () => import('/@/views/demo/page/form/high/index.vue'),
+          meta: {
+            title: '高级表单',
+          },
+        },
+      ],
+    },
+    // =============================form end=============================
+
     // =============================result start=============================
     {
       path: '/result',

+ 8 - 0
src/router/routes/modules/demo/table.ts

@@ -127,6 +127,14 @@ const table: AppRouteModule = {
         title: '可编辑单元格',
       },
     },
+    {
+      path: '/editRowTable',
+      name: 'EditRowTableDemo',
+      component: () => import('/@/views/demo/table/EditRowTable.vue'),
+      meta: {
+        title: '可编辑行',
+      },
+    },
   ],
 };
 

+ 0 - 1
src/views/demo/page/account/center/Project.vue

@@ -51,7 +51,6 @@
 
       img {
         width: 100%;
-        height: 100px;
       }
 
       &-title {

+ 119 - 0
src/views/demo/page/form/basic/data.ts

@@ -0,0 +1,119 @@
+import { FormSchema } from '/@/components/Form';
+
+export const schemas: FormSchema[] = [
+  {
+    field: 'title',
+    component: 'Input',
+    label: '标题',
+    componentProps: {
+      placeholder: '给目标起个名字',
+    },
+    required: true,
+  },
+  {
+    field: 'time',
+    component: 'RangePicker',
+    label: '起止日期',
+    required: true,
+  },
+  {
+    field: 'target',
+    component: 'InputTextArea',
+    label: '目标描述',
+    componentProps: {
+      placeholder: '请输入你的阶段性工作目标',
+      rows: 4,
+    },
+    required: true,
+  },
+  {
+    field: 'metrics',
+    component: 'InputTextArea',
+    label: '衡量标准',
+    componentProps: {
+      placeholder: '请输入衡量标准',
+      rows: 4,
+    },
+    required: true,
+  },
+  {
+    field: 'client',
+    component: 'Input',
+    label: '客户',
+    helpMessage: '目标的服务对象',
+    subLabel: '( 选填 )',
+    componentProps: {
+      placeholder: '请描述你服务的客户,内部客户直接 @姓名/工号',
+    },
+  },
+  {
+    field: 'inviteer',
+    component: 'Input',
+    label: '邀评人',
+    subLabel: '( 选填 )',
+    componentProps: {
+      placeholder: '请直接 @姓名/工号,最多可邀请 5 人',
+    },
+  },
+  {
+    field: 'weights',
+    component: 'InputNumber',
+    label: '权重',
+    subLabel: '( 选填 )',
+    componentProps: {
+      formatter: (value: string) => (value ? `${value}%` : ''),
+      parser: (value: string) => value.replace('%', ''),
+      placeholder: '请输入',
+    },
+  },
+  {
+    field: 'disclosure',
+    component: 'RadioGroup',
+    label: '目标公开',
+    itemProps: {
+      extra: '客户、邀评人默认被分享',
+    },
+    componentProps: {
+      options: [
+        {
+          label: '公开',
+          value: '1',
+        },
+        {
+          label: '部分公开',
+          value: '2',
+        },
+        {
+          label: '不公开',
+          value: '3',
+        },
+      ],
+    },
+  },
+  {
+    field: 'disclosurer',
+    component: 'Select',
+    label: ' ',
+    show: ({ model }) => {
+      return model.disclosure === '2';
+    },
+    componentProps: {
+      placeholder: '公开给',
+      mode: 'multiple',
+      options: [
+        {
+          label: '同事1',
+          value: '1',
+        },
+        {
+          label: '同事2',
+          value: '2',
+        },
+        {
+          label: '同事3',
+          value: '3',
+        },
+      ],
+    },
+  },
+];

+ 66 - 0
src/views/demo/page/form/basic/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <div>
+    <a-page-header title="基础表单" :ghost="false">
+      表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。
+    </a-page-header>
+
+    <div class="m-5 form-wrap">
+      <BasicForm @register="register" />
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { defineComponent } from 'vue';
+  import { schemas } from './data';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  export default defineComponent({
+    components: { BasicForm },
+    setup() {
+      const { createMessage } = useMessage();
+      const [register, { validate, setProps }] = useForm({
+        labelCol: {
+          span: 7,
+        },
+        wrapperCol: {
+          span: 10,
+        },
+        schemas: schemas,
+        actionColOptions: {
+          offset: 8,
+        },
+        submitButtonOptions: {
+          text: '提交',
+        },
+        submitFunc: customSubmitFunc,
+      });
+
+      async function customSubmitFunc() {
+        try {
+          await validate();
+          setProps({
+            submitButtonOptions: {
+              loading: true,
+            },
+          });
+          setTimeout(() => {
+            setProps({
+              submitButtonOptions: {
+                loading: false,
+              },
+            });
+            createMessage.success('提交成功!');
+          }, 2000);
+        } catch (error) {}
+      }
+
+      return { register };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .form-wrap {
+    padding: 24px;
+    background: #fff;
+  }
+</style>

+ 142 - 0
src/views/demo/page/form/high/PersonTable.vue

@@ -0,0 +1,142 @@
+<template>
+  <div>
+    <BasicTable @register="registerTable">
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+    </BasicTable>
+    <a-button block class="mt-5" type="dashed" @click="handleAdd">新增成员</a-button>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import {
+    BasicTable,
+    useTable,
+    TableAction,
+    BasicColumn,
+    ActionItem,
+    renderEditableRow,
+    EditTableHeaderIcon,
+    EditRecordRow,
+  } from '/@/components/Table';
+
+  const columns: BasicColumn[] = [
+    {
+      title: '成员姓名',
+      dataIndex: 'name',
+      customRender: renderEditableRow({ dataIndex: 'name', placeholder: '请输入成员姓名' }),
+    },
+    {
+      title: '工号',
+      dataIndex: 'no',
+      customRender: renderEditableRow({ dataIndex: 'no', placeholder: '请输入工号' }),
+    },
+    {
+      title: '所属部门',
+      dataIndex: 'dept',
+      customRender: renderEditableRow({ dataIndex: 'dept', placeholder: '请输入所属部门' }),
+    },
+  ];
+
+  const data: any[] = [
+    {
+      name: 'John Brown',
+      no: '00001',
+      dept: 'New York No. 1 Lake Park',
+    },
+    {
+      name: 'John Brown2',
+      no: '00002',
+      dept: 'New York No. 2 Lake Park',
+    },
+    {
+      name: 'John Brown3',
+      no: '00003',
+      dept: 'New York No. 3Lake Park',
+    },
+  ];
+  export default defineComponent({
+    components: { BasicTable, EditTableHeaderIcon, TableAction },
+    setup() {
+      const [registerTable, { getDataSource }] = useTable({
+        columns: columns,
+        showIndexColumn: false,
+        dataSource: data,
+        actionColumn: {
+          width: 160,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+        },
+        pagination: false,
+      });
+
+      function handleEdit(record: EditRecordRow) {
+        record.editable = true;
+      }
+
+      function handleCancel(record: EditRecordRow) {
+        record.editable = false;
+        record.onCancel && record.onCancel();
+        if (record.isNew) {
+          const data = getDataSource();
+          const index = data.findIndex((item) => item.key === record.key);
+          data.splice(index, 1);
+        }
+      }
+
+      function handleSave(record: EditRecordRow) {
+        record.editable = false;
+        record.onSubmit && record.onSubmit();
+      }
+
+      function handleAdd() {
+        const data = getDataSource();
+        const addRow: EditRecordRow = {
+          name: '',
+          no: '',
+          dept: '',
+          editable: true,
+          isNew: true,
+        };
+        data.push(addRow);
+      }
+
+      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
+        if (!record.editable) {
+          return [
+            {
+              label: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              label: '删除',
+            },
+          ];
+        }
+        return [
+          {
+            label: '保存',
+            onClick: handleSave.bind(null, record, column),
+          },
+          {
+            label: '取消',
+            popConfirm: {
+              title: '是否取消编辑',
+              confirm: handleCancel.bind(null, record, column),
+            },
+          },
+        ];
+      }
+
+      return {
+        registerTable,
+        handleEdit,
+        createActions,
+        handleAdd,
+        getDataSource,
+      };
+    },
+  });
+</script>

+ 149 - 0
src/views/demo/page/form/high/data.ts

@@ -0,0 +1,149 @@
+import { FormSchema } from '/@/components/Form';
+
+const basicOptions: SelectOptions = [
+  {
+    label: '付晓晓',
+    value: '1',
+  },
+  {
+    label: '周毛毛',
+    value: '2',
+  },
+];
+
+const storeTypeOptions: SelectOptions = [
+  {
+    label: '私密',
+    value: '1',
+  },
+  {
+    label: '公开',
+    value: '2',
+  },
+];
+
+export const schemas: FormSchema[] = [
+  {
+    field: 'f1',
+    component: 'Input',
+    label: '仓库名',
+    required: true,
+  },
+  {
+    field: 'f2',
+    component: 'Input',
+    label: '仓库域名',
+    required: true,
+    componentProps: {
+      addonBefore: 'http://',
+      addonAfter: 'com',
+    },
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 'f3',
+    component: 'Select',
+    label: '仓库管理员',
+    componentProps: {
+      options: basicOptions,
+    },
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 'f4',
+    component: 'Select',
+    label: '审批人',
+    componentProps: {
+      options: basicOptions,
+    },
+    required: true,
+  },
+  {
+    field: 'f5',
+    component: 'RangePicker',
+    label: '生效日期',
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 'f6',
+    component: 'Select',
+    label: '仓库类型',
+    componentProps: {
+      options: storeTypeOptions,
+    },
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+];
+export const taskSchemas: FormSchema[] = [
+  {
+    field: 't1',
+    component: 'Input',
+    label: '任务名',
+    required: true,
+  },
+  {
+    field: 't2',
+    component: 'Input',
+    label: '任务描述',
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 't3',
+    component: 'Select',
+    label: '执行人',
+    componentProps: {
+      options: basicOptions,
+    },
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 't4',
+    component: 'Select',
+    label: '责任人',
+    componentProps: {
+      options: basicOptions,
+    },
+    required: true,
+  },
+  {
+    field: 't5',
+    component: 'TimePicker',
+    label: '生效日期',
+    required: true,
+    componentProps: {
+      style: { width: '100%' },
+    },
+    colProps: {
+      offset: 2,
+    },
+  },
+  {
+    field: 't6',
+    component: 'Select',
+    label: '任务类型',
+    componentProps: {
+      options: storeTypeOptions,
+    },
+    required: true,
+    colProps: {
+      offset: 2,
+    },
+  },
+];

+ 72 - 0
src/views/demo/page/form/high/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="high-form">
+    <a-page-header title="高级表单" :ghost="false">
+      高级表单常见于一次性输入和提交大批量数据的场景。
+    </a-page-header>
+
+    <div class="m-5">
+      <a-card title="仓库管理" :bordered="false">
+        <BasicForm @register="register" layout="vertical" />
+      </a-card>
+      <a-card title="任务管理" :bordered="false" class="mt-5">
+        <BasicForm @register="registerTask" layout="vertical" />
+      </a-card>
+      <a-card title="成员管理" :bordered="false" class="mt-5">
+        <PersonTable ref="tableRef" />
+      </a-card>
+    </div>
+
+    <app-footer>
+      <template #right>
+        <a-button type="primary" @click="submitAll">提交</a-button>
+      </template>
+    </app-footer>
+  </div>
+</template>
+<script lang="ts">
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { defineComponent, ref } from 'vue';
+  import PersonTable from './PersonTable.vue';
+  import { schemas, taskSchemas } from './data';
+
+  export default defineComponent({
+    components: { BasicForm, PersonTable },
+    setup() {
+      const tableRef = ref<{ getDataSource: () => any } | null>(null);
+
+      const [register, { validate }] = useForm({
+        baseColProps: {
+          span: 6,
+        },
+        schemas: schemas,
+        showActionButtonGroup: false,
+      });
+
+      const [registerTask, { validate: validateTaskForm }] = useForm({
+        baseColProps: {
+          span: 6,
+        },
+        schemas: taskSchemas,
+        showActionButtonGroup: false,
+      });
+
+      async function submitAll() {
+        try {
+          if (tableRef.value) {
+            console.log('table data:', tableRef.value.getDataSource());
+          }
+
+          const [values, taskValues] = await Promise.all([validate(), validateTaskForm()]);
+          console.log('form data:', values, taskValues);
+        } catch (error) {}
+      }
+
+      return { register, registerTask, submitAll, tableRef };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .high-form {
+    padding-bottom: 48px;
+  }
+</style>

+ 95 - 0
src/views/demo/page/form/step/Step1.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="step1">
+    <div class="step1-form">
+      <BasicForm @register="register">
+        <template #fac="{ model, field }">
+          <a-input-group compact>
+            <a-select v-model:value="model['pay']" class="pay-select">
+              <a-select-option value="zfb"> 支付宝 </a-select-option>
+              <a-select-option value="yl"> 银联 </a-select-option>
+            </a-select>
+            <a-input class="pay-input" v-model:value="model[field]" />
+          </a-input-group>
+        </template>
+      </BasicForm>
+    </div>
+    <a-divider />
+    <h3>说明</h3>
+    <h4>转账到支付宝账户</h4>
+    <p>
+      如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
+    </p>
+
+    <h4>转账到银行卡</h4>
+    <p>
+      如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
+    </p>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { step1Schemas } from './data';
+
+  export default defineComponent({
+    components: { BasicForm },
+    emits: ['next'],
+    setup(_, { emit }) {
+      const [register, { validate }] = useForm({
+        labelWidth: 100,
+        schemas: step1Schemas,
+        actionColOptions: {
+          span: 14,
+        },
+        showResetButton: false,
+        submitButtonOptions: {
+          text: '下一步',
+        },
+        submitFunc: customSubmitFunc,
+      });
+
+      async function customSubmitFunc() {
+        try {
+          const values = await validate();
+          emit('next', values);
+        } catch (error) {}
+      }
+
+      return { register };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .step1 {
+    &-form {
+      width: 450px;
+      margin: 0 auto;
+    }
+
+    h3 {
+      margin: 0 0 12px;
+      font-size: 16px;
+      line-height: 32px;
+      color: rgba(0, 0, 0, 0.45);
+    }
+
+    h4 {
+      margin: 0 0 4px;
+      font-size: 14px;
+      line-height: 22px;
+      color: rgba(0, 0, 0, 0.45);
+    }
+
+    p {
+      color: rgba(0, 0, 0, 0.45);
+    }
+  }
+
+  .pay-select {
+    width: 20%;
+  }
+
+  .pay-input {
+    width: 70%;
+  }
+</style>

+ 71 - 0
src/views/demo/page/form/step/Step2.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="step2">
+    <a-alert message="确认转账后,资金将直接打入对方账户,无法退回。" show-icon />
+    <a-descriptions :column="1" class="mt-5">
+      <a-descriptions-item label="付款账户"> ant-design@alipay.com </a-descriptions-item>
+      <a-descriptions-item label="收款账户"> test@example.com </a-descriptions-item>
+      <a-descriptions-item label="收款人姓名"> Vben </a-descriptions-item>
+      <a-descriptions-item label="转账金额"> 500元 </a-descriptions-item>
+    </a-descriptions>
+    <a-divider />
+    <BasicForm @register="register" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { step2Schemas } from './data';
+
+  export default defineComponent({
+    components: { BasicForm },
+    emits: ['next', 'prev'],
+    setup(_, { emit }) {
+      const [register, { validate, setProps }] = useForm({
+        labelWidth: 80,
+        schemas: step2Schemas,
+        actionColOptions: {
+          span: 14,
+        },
+        resetButtonOptions: {
+          text: '上一步',
+        },
+        submitButtonOptions: {
+          text: '提交',
+        },
+        resetFunc: customResetFunc,
+        submitFunc: customSubmitFunc,
+      });
+
+      async function customResetFunc() {
+        emit('prev');
+      }
+
+      async function customSubmitFunc() {
+        try {
+          const values = await validate();
+          setProps({
+            submitButtonOptions: {
+              loading: true,
+            },
+          });
+          setTimeout(() => {
+            setProps({
+              submitButtonOptions: {
+                loading: false,
+              },
+            });
+            emit('next', values);
+          }, 1500);
+        } catch (error) {}
+      }
+
+      return { register };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .step2 {
+    width: 450px;
+    margin: 0 auto;
+  }
+</style>

+ 45 - 0
src/views/demo/page/form/step/Step3.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="step3">
+    <a-result status="success" title="操作成功" sub-title="预计两小时内到账">
+      <template #extra>
+        <a-button type="primary" @click="redo">再转一笔 </a-button>
+        <a-button> 查看账单 </a-button>
+      </template>
+    </a-result>
+    <div class="desc-wrap">
+      <a-descriptions :column="1" class="mt-5">
+        <a-descriptions-item label="付款账户"> ant-design@alipay.com </a-descriptions-item>
+        <a-descriptions-item label="收款账户"> test@example.com </a-descriptions-item>
+        <a-descriptions-item label="收款人姓名"> Vben </a-descriptions-item>
+        <a-descriptions-item label="转账金额"> 500元 </a-descriptions-item>
+      </a-descriptions>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({
+    components: {},
+    emits: ['redo'],
+    setup(_, { emit }) {
+      return {
+        redo: () => {
+          emit('redo');
+        },
+      };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .step3 {
+    width: 600px;
+    margin: 0 auto;
+  }
+
+  .desc-wrap {
+    padding: 24px 40px;
+    margin-top: 24px;
+    background: #fafafa;
+  }
+</style>

+ 63 - 0
src/views/demo/page/form/step/data.tsx

@@ -0,0 +1,63 @@
+import { FormSchema } from '/@/components/Form';
+
+export const step1Schemas: FormSchema[] = [
+  {
+    field: 'account',
+    component: 'Select',
+    label: '付款账户',
+    required: true,
+    defaultValue: '1',
+    componentProps: {
+      options: [
+        {
+          label: 'anncwb@126.com',
+          value: '1',
+        },
+      ],
+    },
+  },
+  {
+    field: 'fac',
+    component: 'InputGroup',
+    label: '收款账户',
+    required: true,
+    defaultValue: 'test@example.com',
+    slot: 'fac',
+  },
+  {
+    field: 'pay',
+    component: 'Input',
+    label: '',
+    defaultValue: 'zfb',
+    show: false,
+  },
+  {
+    field: 'payeeName',
+    component: 'Input',
+    label: '收款人姓名',
+    defaultValue: 'Vben',
+    required: true,
+  },
+  {
+    field: 'money',
+    component: 'Input',
+    label: '转账金额',
+    defaultValue: '500',
+    required: true,
+    renderComponentContent: () => {
+      return {
+        prefix: () => '¥',
+      };
+    },
+  },
+];
+
+export const step2Schemas: FormSchema[] = [
+  {
+    field: 'pwd',
+    component: 'InputPassword',
+    label: '支付密码',
+    required: true,
+    defaultValue: '123456',
+  },
+];

+ 86 - 0
src/views/demo/page/form/step/index.vue

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <a-page-header title="分步表单" :ghost="false">
+      将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。
+    </a-page-header>
+
+    <div class="m-5 step-form-content">
+      <div class="step-form-form">
+        <a-steps :current="current">
+          <a-step title="填写转账信息"> </a-step>
+          <a-step title="确认转账信息"> </a-step>
+          <a-step title="完成"> </a-step>
+        </a-steps>
+      </div>
+      <div class="mt-5">
+        <Step1 @next="handleStep1Next" v-show="current === 0" />
+        <Step2
+          @prev="handleStepPrev"
+          @next="handleStep2Next"
+          v-show="current === 1"
+          v-if="initSetp2"
+        />
+        <Step3 v-show="current === 2" @redo="handleRedo" v-if="initSetp3" />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, reactive, toRefs } from 'vue';
+  import Step1 from './Step1.vue';
+  import Step2 from './Step2.vue';
+  import Step3 from './Step3.vue';
+  export default defineComponent({
+    components: { Step1, Step2, Step3 },
+    setup() {
+      const current = ref(0);
+
+      const state = reactive({
+        initSetp2: false,
+        initSetp3: false,
+      });
+
+      function handleStep1Next(step1Values: any) {
+        current.value++;
+        state.initSetp2 = true;
+        console.log(step1Values);
+      }
+
+      function handleStepPrev() {
+        current.value--;
+      }
+
+      function handleStep2Next(step2Values: any) {
+        current.value++;
+        state.initSetp3 = true;
+        console.log(step2Values);
+      }
+
+      function handleRedo() {
+        current.value = 0;
+        state.initSetp2 = false;
+        state.initSetp3 = false;
+      }
+
+      return {
+        current,
+        handleStep1Next,
+        handleStep2Next,
+        handleRedo,
+        handleStepPrev,
+        ...toRefs(state),
+      };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  .step-form-content {
+    padding: 24px;
+    background: #fff;
+  }
+
+  .step-form-form {
+    width: 750px;
+    margin: 0 auto;
+  }
+</style>

+ 105 - 0
src/views/demo/table/EditRowTable.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="p-4">
+    <BasicTable @register="registerTable">
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+    </BasicTable>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import {
+    BasicTable,
+    useTable,
+    TableAction,
+    BasicColumn,
+    ActionItem,
+    renderEditableRow,
+    EditTableHeaderIcon,
+    EditRecordRow,
+  } from '/@/components/Table';
+
+  import { demoListApi } from '/@/api/demo/table';
+  const columns: BasicColumn[] = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      customRender: renderEditableRow({ dataIndex: 'id' }),
+    },
+    {
+      title: '姓名',
+      dataIndex: 'name',
+      customRender: renderEditableRow({
+        dataIndex: 'name',
+      }),
+    },
+  ];
+  export default defineComponent({
+    components: { BasicTable, EditTableHeaderIcon, TableAction },
+    setup() {
+      const currentEditKeyRef = ref('');
+
+      const [registerTable] = useTable({
+        title: '可编辑行示例',
+        api: demoListApi,
+        columns: columns,
+        showIndexColumn: false,
+        actionColumn: {
+          width: 160,
+          title: 'Action',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+        },
+      });
+
+      function handleEdit(record: EditRecordRow) {
+        currentEditKeyRef.value = record.key;
+        record.editable = true;
+      }
+
+      function handleCancel(record: EditRecordRow) {
+        currentEditKeyRef.value = '';
+        record.editable = false;
+        record.onCancel && record.onCancel();
+      }
+
+      function handleSave(record: EditRecordRow) {
+        currentEditKeyRef.value = '';
+        record.editable = false;
+        record.onSubmit && record.onSubmit();
+      }
+
+      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
+        if (!record.editable) {
+          return [
+            {
+              label: '编辑',
+              disabled: currentEditKeyRef.value ? currentEditKeyRef.value !== record.key : false,
+              onClick: handleEdit.bind(null, record),
+            },
+          ];
+        }
+        return [
+          {
+            label: '保存',
+            onClick: handleSave.bind(null, record, column),
+          },
+          {
+            label: '取消',
+            popConfirm: {
+              title: '是否取消编辑',
+              confirm: handleCancel.bind(null, record, column),
+            },
+          },
+        ];
+      }
+
+      return {
+        registerTable,
+        handleEdit,
+        createActions,
+      };
+    },
+  });
+</script>

+ 7 - 7
src/views/demo/table/FixedColumn.vue

@@ -1,18 +1,18 @@
 <template>
   <div class="p-4">
     <BasicTable @register="registerTable">
-      <template #action>
+      <template #action="{ record }">
         <TableAction
           :actions="[
             {
               label: '删除',
-              onClick: handleDelete,
+              onClick: handleDelete.bind(null, record),
             },
           ]"
           :dropDownActions="[
             {
               label: '启用',
-              onClick: handleOpen,
+              onClick: handleOpen.bind(null, record),
             },
           ]"
         />
@@ -73,11 +73,11 @@
           slots: { customRender: 'action' },
         },
       });
-      function handleDelete() {
-        console.log('点击了删除');
+      function handleDelete(record: any) {
+        console.log('点击了删除', record);
       }
-      function handleOpen() {
-        console.log('点击了启用');
+      function handleOpen(record: any) {
+        console.log('点击了启用', record);
       }
       return {
         registerTable,

+ 36 - 36
yarn.lock

@@ -1051,9 +1051,9 @@
   integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
 
 "@iconify/json@^1.1.258":
-  version "1.1.258"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.258.tgz#392064ae8fd4c6d542c21bb4d0d57d5860f38abb"
-  integrity sha512-x5DKhRrg8v1NWmClWa8zA80gWQ9xevivsUAF4s8CyAl/ZplBsEE1funKuuVcIKjexyE1UXb7uFWrUKt1fB5n1A==
+  version "1.1.259"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.259.tgz#433ccff8572e42c3abab1cdbac872af0e8340419"
+  integrity sha512-tDL4IgKtj3LA2lI+IoZfylBtKWNpV5I9BLj7WGMI/SvGkkYo+9DUap3Ho6iUbQLtu9GmHwpE/cA8mwEBkj/otw==
 
 "@koa/cors@^3.1.0":
   version "3.1.0"
@@ -1236,9 +1236,9 @@
     unist-util-find-all-after "^3.0.1"
 
 "@surma/rollup-plugin-off-main-thread@^1.1.1":
-  version "1.4.1"
-  resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.1.tgz#bf1343e5a926e5a1da55e3affd761dda4ce143ef"
-  integrity sha512-ZPBWYQDdO4JZiTmTP3DABsHhIPA7bEJk9Znk7tZsrbPGanoGo8YxMv//WLx5Cvb+lRgS42+6yiOIYYHCKDmkpQ==
+  version "1.4.2"
+  resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz#e6786b6af5799f82f7ab3a82e53f6182d2b91a58"
+  integrity sha512-yBMPqmd1yEJo/280PAMkychuaALyQ9Lkb5q1ck3mjJrFuEobIfhnQ4J3mbvBoISmR3SWMWV+cGB/I0lCQee79A==
   dependencies:
     ejs "^2.6.1"
     magic-string "^0.25.0"
@@ -1850,7 +1850,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
-ant-design-vue@^2.0.0-rc.1:
+ant-design-vue@^2.0.0-beta.15:
   version "2.0.0-rc.1"
   resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-rc.1.tgz#2ef02475f3aa4c1474f2fe3cf44a52c34787be02"
   integrity sha512-iKXkFtTHarvLHV7LWmYh6g/Cmkv+xK+vS621A1Qvg37Z6lCGg3K9BGAizmklAYzOTiPz0Ltt63eSiNqYMGh52g==
@@ -1982,9 +1982,9 @@ astral-regex@^2.0.0:
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
 async-validator@^3.3.0, async-validator@^3.4.0:
-  version "3.4.0"
-  resolved "https://registry.npmjs.org/async-validator/-/async-validator-3.4.0.tgz#871b3e594124bf4c4eb7bcd1a9e78b44f3b09cae"
-  integrity sha512-VrFk4eYiJAWKskEz115iiuCf9O0ftnMMPXrOFMqyzGH2KxO7YwncKyn/FgOOP+0MDHMfXL7gLExagCutaZGigA==
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/async-validator/-/async-validator-3.5.0.tgz#f6791ee7217cde8036941591bc3754f7c26bbf89"
+  integrity sha512-jMDcDHrH618eznoO4/3afJG5+I4HE/ipQd7y4mhPJmCaoHCSPOJfjpWgjFoxma2h8irL+zGe+qwyptDrR37Vhg==
 
 async@^2.6.2:
   version "2.6.3"
@@ -2079,9 +2079,9 @@ balanced-match@^1.0.0:
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
 base64-js@^1.3.1:
-  version "1.5.0"
-  resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.0.tgz#2d03045876d9e2b68a7a0f87d6bd163595e3b6af"
-  integrity sha512-Jrdy04F2EKcNggUDfubMUPNAZg2vMquLQSm8sKLYJvz40ClFL1S8GKyDshGkNsbNNE5Z+fQavzU7nSK1I9JUGA==
+  version "1.5.1"
+  resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 base@^0.11.1:
   version "0.11.2"
@@ -2297,9 +2297,9 @@ camelcase@^5.0.0, camelcase@^5.3.1:
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
 caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157:
-  version "1.0.30001157"
-  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz#2d11aaeb239b340bc1aa730eca18a37fdb07a9ab"
-  integrity sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==
+  version "1.0.30001158"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001158.tgz#fce86d321369603c2bc855ee0e901a7f49f8310b"
+  integrity sha512-s5loVYY+yKpuVA3HyW8BarzrtJvwHReuzugQXlv1iR3LKSReoFXRm86mT6hT7PEF5RxW+XQZg+6nYjlywYzQ+g==
 
 ccount@^1.0.0:
   version "1.1.0"
@@ -2940,9 +2940,9 @@ cssesc@^3.0.0:
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
 csstype@^2.6.8:
-  version "2.6.13"
-  resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
-  integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
+  version "2.6.14"
+  resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de"
+  integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==
 
 currently-unhandled@^0.4.1:
   version "0.4.1"
@@ -3263,9 +3263,9 @@ ejs@^2.6.1:
   integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
 
 electron-to-chromium@^1.3.591:
-  version "1.3.592"
-  resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz#4621521b223bf6e5469373528321e185d3c24670"
-  integrity sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==
+  version "1.3.596"
+  resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.596.tgz#c7ed98512c7ff36ddcbfed9e54e6355335c35257"
+  integrity sha512-nLO2Wd2yU42eSoNJVQKNf89CcEGqeFZd++QsnN2XIgje1s/19AgctfjLIbPORlvcCO8sYjLwX4iUgDdusOY8Sg==
 
 emoji-regex@^7.0.1:
   version "7.0.3"
@@ -6931,9 +6931,9 @@ rollup@^1.31.1:
     acorn "^7.1.0"
 
 rollup@^2.32.1:
-  version "2.33.1"
-  resolved "https://registry.npmjs.org/rollup/-/rollup-2.33.1.tgz#802795164164ee63cd47769d8879c33ec8ae0c40"
-  integrity sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w==
+  version "2.33.2"
+  resolved "https://registry.npmjs.org/rollup/-/rollup-2.33.2.tgz#c4c76cd405a7605e6ebe90976398c46d4c2ea166"
+  integrity sha512-QPQ6/fWCrzHtSXkI269rhKaC7qXGghYBwXU04b1JsDZ6ibZa3DJ9D1SFAYRMgx1inDg0DaTbb3N4Z1NK/r3fhw==
   optionalDependencies:
     fsevents "~2.1.2"
 
@@ -7874,9 +7874,9 @@ type-fest@^0.13.1:
   integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
 
 type-fest@^0.18.0:
-  version "0.18.0"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.0.tgz#2edfa6382d48653707344f7fccdb0443d460e8d6"
-  integrity sha512-fbDukFPnJBdn2eZ3RR+5mK2slHLFd6gYHY7jna1KWWy4Yr4XysHuCdXRzy+RiG/HwG4WJat00vdC2UHky5eKiQ==
+  version "0.18.1"
+  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
 
 type-fest@^0.3.1:
   version "0.3.1"
@@ -7919,9 +7919,9 @@ typescript@^4.0.5:
   integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
 
 uglify-js@^3.1.4:
-  version "3.11.5"
-  resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.5.tgz#d6788bc83cf35ff18ea78a65763e480803409bc6"
-  integrity sha512-btvv/baMqe7HxP7zJSF7Uc16h1mSfuuSplT0/qdjxseesDU+yYzH33eHBH+eMdeRXwujXspaCTooWHQVVBh09w==
+  version "3.11.6"
+  resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.6.tgz#144b50d3e05eadd3ad4dd047c60ca541a8cd4e9c"
+  integrity sha512-oASI1FOJ7BBFkSCNDZ446EgkSuHkOZBuqRFrwXIKWCoXw8ZXQETooTQjkAcBS03Acab7ubCKsXnwuV2svy061g==
 
 unherit@^1.0.4:
   version "1.1.3"
@@ -8581,7 +8581,7 @@ y18n@^4.0.0:
   resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
   integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
 
-y18n@^5.0.2:
+y18n@^5.0.5:
   version "5.0.5"
   resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
   integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
@@ -8656,16 +8656,16 @@ yargs@^15.0.0, yargs@^15.1.0:
     yargs-parser "^18.1.2"
 
 yargs@^16.0.3, yargs@^16.1.0:
-  version "16.1.0"
-  resolved "https://registry.npmjs.org/yargs/-/yargs-16.1.0.tgz#fc333fe4791660eace5a894b39d42f851cd48f2a"
-  integrity sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==
+  version "16.1.1"
+  resolved "https://registry.npmjs.org/yargs/-/yargs-16.1.1.tgz#5a4a095bd1ca806b0a50d0c03611d38034d219a1"
+  integrity sha512-hAD1RcFP/wfgfxgMVswPE+z3tlPFtxG8/yWUrG2i17sTWGCGqWnxKcLTF4cUKDUK8fzokwsmO9H0TDkRbMHy8w==
   dependencies:
     cliui "^7.0.2"
     escalade "^3.1.1"
     get-caller-file "^2.0.5"
     require-directory "^2.1.1"
     string-width "^4.2.0"
-    y18n "^5.0.2"
+    y18n "^5.0.5"
     yargs-parser "^20.2.2"
 
 ylru@^1.2.0: