Ant Design Vue 使用过程问题记录

536 阅读7分钟

配置主题

scss写法

src/styles/theme.variable.scss

$primaryColor: #1677ff;
$white: #ffffff;
$black: #000000;

导出给themeConfig.ts使用 src/styles/theme.module.scss

:export {
  themeColor: $primaryColor;
}

src/styles/themeConfig.ts

/**
 * 主题色
 * 用途1: ant-d 组件库的主题
 * 用途2: tailwind 的预设变量
 * 用途3: less 的预设变量
 */
import variables from './theme.module.scss'

const themeConfig = {
  token: {
    colorPrimary: variables.themeColor
  }
} as const
// Object.freeze(themeConfig)
export default themeConfig

less写法

src/styles/themeConfig.ts

/**
 * 主题色
 * 用途1: ant-d 组件库的主题
 * 用途2: tailwind 的预设变量
 * 用途3: less 的预设变量
 */
const themeConfig = {
  token: {
    colorPrimary: '#296bcb',
    colorSuccess: '#56b460',
    colorWarning: '#e8b533',
    colorError: '#e83345',
    colorTextBase: '#c1d5ff',
    colorBgContainer: '#21293d',
    colorBgElevated: '#0b162f',
    colorBgSpotlight: '#293550',
    colorBgBase: '#1c2d46',
    fontSize: 16,
    wireframe: false,
    colorPrimaryBg: '#296bcb',
    colorPrimaryText: '#94fcfd',
    colorPrimaryTextActive: '#94fcfd',
    borderRadius: 1,
    borderRadiusSM: 4,
    borderRadiusLG: 6,
    borderRadiusXS: 2,

    // 自定义颜色表
    colorWhite: '#fff',
    colorWhite2: '#E3E4E4',

    colorBlue1: 'rgba(41, 107, 203, 0.15)',
    colorBlue2: 'rgba(41, 107, 203, 0.3)',
    colorBlue3: '#0E477B',
    colorBlue4: '#AAD1F3',
    colorBlue5: '#B2C0CD',
    colorBlue6: '#37DBF3',
    colorBlue7: '#3e82e4',
    colorBlue8: '#0B1730',
    colorBlue9: 'rgba(11, 22, 47, 0.80)',
    colorBlue10: '#283960',
    colorBlue11: '#66C4FF',
    colorBlue12: '#74b2ff',
    colorBlue13: '#0C1932',
    colorBlue14: 'rgb(102, 196, 255, 0.1)',
    colorBlue15: 'rgb(102, 196, 255, 0.2)',
    colorBlue16: 'rgb(102, 196, 255, 0.4)',
    colorBlue17: 'rgb(193, 213, 255, 0.90)',
    colorBlue18: 'rgb(33, 96, 172, 0.30)',
    colorBlue19: 'rgb(116, 178, 255, 0.4)',

    colorPurple1: '#8899FA',

    colorRed1: 'rgba(232, 51, 69, 0.20)',
    colorRed2: 'rgba(232, 51, 69, 0.10)',
    colorRed3: 'rgba(232, 51, 69, 0.40)',
    colorRed4: '#FFC2C8',

    colorYellow1: 'rgba(232, 181, 51, 0.10)',
    colorYellow2: 'rgba(232, 181, 51, 0.20)',
    colorYellow3: 'rgba(232, 181, 51, 0.40)',

    // 自定义渐变色表
    colorLinearGradient1: 'linear-gradient(180deg, #ECF9FF 0%, #90D8FC 100%)',
    colorLinearGradient2: 'linear-gradient(180deg, #296bcb 0%, #1553ad 100%)',
    colorLinearGradient3: 'linear-gradient(180deg, #153C73 0%, #153C73 100%)',
    colorLinearGradient4:
      'linear-gradient(180deg, #FF1321 0%, #F6EA32 38%, #25C970 70.5%, #132AFF 100%)',
    colorLinearGradient5: 'linear-gradient(180deg, #E6F7FF 0%, #3CB1FB 100%)',
  },
  algorithm: [null],
} as const;
Object.freeze(themeConfig);
export default themeConfig;

/src/App.vue

<script lang="ts" setup>
import themeConfig from '@/design/themeConfig';
import { theme, legacyLogicalPropertiesTransformer } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import { RouterView } from 'vue-router';

defineOptions({ name: 'App' });
const {
  VITE_PREFIX_CLASSNAME,
} = import.meta.env;

// antD 全局配置
const antDConfig = {
  locale: zhCN,
  prefixCls: VITE_PREFIX_CLASSNAME,
  componentSize: 'middle',
  theme: {
    ...themeConfig,
    algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
  },
} as const;


</script>

<template>
  <a-style-provider hash-priority="high" :transformers="[legacyLogicalPropertiesTransformer]">
    <a-config-provider v-bind="antDConfig">
        <RouterView />
    </a-config-provider>
  </a-style-provider>
</template>

<style>
@import '@/design/antd.less';
</style>

<style module>
.app {
  @apply h-screen w-screen text-TextBase select-none pointer-events-none fullAbsolute;
  font-size: 0.875rem;
}
</style>

配置命名空间

/.env

# antd 的样式名前缀
VITE_PREFIX_CLASSNAME = GD_DKY

/vite.config.ts

import { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// @ts-ignore
import themeConfig from './src/design/themeConfig';

export default defineConfig(({ mode, command }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const { VITE_PREFIX_CLASSNAME } = env;

  return {
    css: {
      preprocessorOptions: {
        // scss: {
        //   javascriptEnabled: true,
        //   additionalData: `@import "@/styles/theme.variable.scss";$antNamespace: ${env.VITE_PREFIX_CLASSNAME};`
        // },
        less: {
          javascriptEnabled: true,
          globalVars: {
            ...themeConfig.token,
            antNamespace: VITE_PREFIX_CLASSNAME,
          },
        },
      },
    },
  };
});

定制组件样式

/src/styles/antd.scss

less 命名空间是 @{antNamespace}

/**
定制 ant-d-vue 的组件样式
 */
#app {
  .#{$antNamespace} {
    //  分段控制器(tab)
    &-segmented {
      // background-color: $colorBlue13;
      // font-size: 0.875rem;

      &-item {
        // border-radius: 1px;

        &:hover,
        &-selected {
          // color: $colorPrimaryTextActive;

          &:not(:hover) {
            // background-color: $colorPrimaryBg;
          }
        }
      }
    }

    // 下拉选择器
    &-select {
      &-selection-item {
        &::after {
          // color: $colorWhite;
        }
      }

      &-dropdown {
        // border-radius: 2px;
        // border: 1px solid $colorBlue3;
      }

      &-item-option {
        // border-radius: 2px;

        &-active,
        &-selected {
          //background: $colorPrimaryBg ;
          // color: $colorPrimaryText;
        }
      }
    }

    //  日期选择器
    &-picker {
      &-dropdown {
        // border-radius: 2px;
        // border: 1px solid $colorBlue3;
      }
    }

    //  播放条
    &-slider {
      &-rail {
        // background-color: $colorBlue13;
      }

      &-track,
      &-handle::after {
        // background-color: $colorWhite2;
      }

      &-handle {
        // top: 50%;
        // transform: translate(-50%, -50%);
      }
    }

    // 表格
    &-table {
      background: unset;

      &-row {
        &:hover {
          td {
            background: unset;
          }
        }
        td {
          background: unset;
        }
      }

      &-cell {
        // padding: 8px 16px;
        // background: unset;
        border: unset;

        a {
          // color: $colorPrimaryText;
        }

        &-fix-left {
          // background: unset;
        }
      }
      &-header {
        border-radius: 0;
      }
      &-thead {
        .#{$antNamespace}-table-cell {
          border-bottom: none;
          // background: $colorBgContainer;

          &::before {
            display: none;
          }

          //  圆角实现
          &:first-child {
            border-top-left-radius: 0px; /* 左上角圆角 */
            border-bottom-left-radius: 0px; /* 右上角圆角 */
          }

          &:last-child {
            border-top-right-radius: 0px; /* 左上角圆角 */
            border-bottom-right-radius: 0px; /* 右上角圆角 */
            box-shadow: unset;
          }
        }
      }
    }

    // 按钮
    &-btn {
      // padding-left: 16px;
      // padding-right: 16px;
      // height: 32px;
      // box-shadow: unset;
      & > span {
        // display: inline-flex;
      }
    }
    &-input-affix-wrapper {
      // height: 32px;
    }
    // 输入框统一宽度、高度
    &-input-affix-wrapper,
    &-select {
      //width: 240px;
      //height: 32px;
      // min-width: 80px;
      // background: $colorBlack1;

      input,
      .#{$antNamespace}-select-selector {
        // background: unset;
        // height: 100%;
        // align-items: center;
      }
    }

    // 表单
    &-form {
      &-item-label > label {
        // height: 100%;
      }
    }

    // 分页
    &-pagination {
      .#{$antNamespace}-select {
        // width: auto;
        // height: auto;
      }

      &-total-text {
        // flex: 1 1 auto;
        // margin-left: 10px;
      }
    }

    // 通知
    &-notification-notice {
      // border-radius: 4px;
      // border-left-width: 4px;
      // border-left-color: transparent;

      &-error {
        // border-left-color: $colorError;
      }
      &-success {
        // border-left-color: $colorSuccess;
      }
      &-info {
        // border-left-color: $colorPrimary;
      }
      &-warning {
        // border-left-color: $colorWarning;
      }
    }

    // 菜单
    &-menu {
      // border-right: none;

      &-item {
        // height: 40px;
        // display: inline-flex;
        // align-items: center;
        // padding: 0 12px;
        // border-radius: 2px;
        // margin: 2px 0;

        &-selected {
          // color: $colorPrimaryTextActive;
        }

        &-icon {
          // margin-right: 10px;
        }
      }
    }

    // 抽屉
    &-drawer {
      &-body {
        // padding: 20px 24px;
      }

      &-content-wrapper {
        // border-left: 1px solid $colorBlue21;
      }

      &-header {
        // padding: 24px;
        // border-color: $colorBlue21;

        &-title {
          // height: 28px;
        }
      }

      &-title {
        // color: $colorWhite;
        // font-size: 20px;
        // font-weight: normal;
      }

      &-close {
        // color: $colorWhite;
        // order: 1;
        // margin-left: 16px;
      }
    }

    // 锚点导航
    &-anchor {
      &-link {
        & > a {
          // color: $colorWhite;
        }

        &-active > a {
          // color: $colorPrimaryTextActive;
        }
      }

      &-ink-visible {
        // width: 1px;
        // background-color: $colorPrimaryTextActive;
      }

      &::before {
        // border-left: 1px $colorBlue21 solid;
      }

      &-wrapper {
      }
    }

    // 描述列表
    &-descriptions {
      &-title {
        // color: $colorWhite;
      }

      //&-view {
      //  border-radius: 2px;
      //}

      &-item {
        &-content {
          // color: $colorWhite;
        }

        &-label {
          // background-color: $colorBlue22;
          // color: $colorWhite3;
        }
      }
    }

    // 文本
    &-typography {
      // margin-bottom: 0;

      &-ellipsis {
        // white-space: normal;
      }
    }

    // 多选框
    &-checkbox {
      &:not(&-checked) .#{$antNamespace}-checkbox-inner {
        // border-color: $colorBlue24;
      }
    }

    // 数字角标
    &-badge-count {
      // background-color: $colorRed5;
      // color: $colorWhite;
    }

    // 时间线
    &-timeline-item-tail {
      // top: 10px;
      // left: 4px;
    }
  }
}

修改默认样式

需要用到vue sytle 的module属性

直接写module 默认是$style, 写module="$glob" 就是$glob

单个类名用:global(){}

多个类名用:global{}

less 命名空间是 @{antNamespace}

<template>
  <div :class="$style.progress">
    <!-- 标题 -->
    <div class="flex justify-between items-center">
      <span>{{ label }}</span>
      <span>{{ value }}</span>
    </div>
    <!-- 进度条 -->
    <a-progress class="bg-white" :percent="percent" :show-info="false" />
  </div>
</template> 
<style lang='less' module>
.progress {
   /* 单个用:global(){}
  :global(.#{$antNamespace}-progress) {
    &-bg {
      background-color: #aff9ff !important;
    }
  }
  
  /* 多个用:global{}
  :global{
      .red {
        color: red;
       }
       .#{$antNamespace}-progress {
            &-bg {
             background-color: #aff9ff !important;
            }
        }
  }
}

</style>

自动按需引入组件

npm install unplugin-vue-components -D /vite.config.js

import { defineConfig } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
  plugins: [
    // ...
    Components({
      resolvers: [
        AntDesignVueResolver({
          importStyle: false, // css in js
        }),
      ],
    }),
  ],
});

1. 表格滚动

  1. 需要给表格的父容器固定宽

  2. 给a-table 绑定 :scroll="{ y: scrollY, x: 'max-content' }" 属性,

  3. scrollY是计算属性, 通过设置flex, 纵向排列, 给表格设置flex: 1,overflow-hidden 计算表格的容器高度clientHeight, 盒子有padding 要减去上下padding

  4. 给列配置某些项绑定具体宽度 width: 200

<div class="flex flex-col">
   <div ref="containerRef" class="flex-1 overflow-hidden"> 
    <a-table pagination="{}" v-model:current="currentPage" v-model:pageSize="pageSize" @change="pageChange" scrollToFirstRowOnChange :scroll="{ y: scrollY - 40, x: 'max-content' }" 
      :columns="service_demand_column" :data-source="tableData">
      <template #bodyCell="{ column, text, record }">
        <!-- 详情 -->
        <template v-if="column.dataIndex === 'operation'">
          <a-button type="link" @click="() => clickDetail(record)">详情</a-button>
        </template>
        <!-- 等级 -->
        <template v-else-if="column.dataIndex === 'warningLevel'">
          <span :class="`text-[${colorList[text]}]`">{{ text || '-' }}</span>
        </template>
        <!-- 其他列 -->
        <template v-else>
          {{ text || '-' }}
        </template>
      </template>
    </a-table>
   </div>
</div>

import useTableHeight from '@/hooks/useTableHeight.hook'

const { containerRef, scrollY } = useTableHeight()

export const service_demand_column = [
  { title: '编号', dataIndex: 'orderNo', fixed: 'left' },
  { title: '等级', dataIndex: 'warningLevel' },
  { title: '分组', dataIndex: 'businessGrouping' },
  { title: '子类', dataIndex: 'businessSub' },
  { title: '发布时间', dataIndex: 'warningTime' },
  { title: '单位', dataIndex: 'powerUnit', width: 100 },
  { title: '发布对象', dataIndex: 'warningObject', width: 120 },
  { title: '销单时间', dataIndex: 'cancelOrderTime' },
  { title: '操作', dataIndex: 'operation', fixed: "right", align: 'center' },
];


// 计算表格高度
import { computed } from 'vue'
const useTableHeight = () => {
  const prefixClassname = import.meta.env.VITE_PREFIX_CLASSNAME
  const headerClassName = `.${prefixClassname}-table-header`
  const paginationClassName = `.${prefixClassname}-table-pagination`

  const containerRef = ref()

  const scrollY = computed<number>(() => {
    const { clientHeight } = containerRef.value || {}
    const header = containerRef.value?.querySelector?.(headerClassName)
    const pagination = containerRef.value?.querySelector?.(paginationClassName)
    const { clientHeight: headerHeight } = header || {}
    const { clientHeight: paginationHeight } = pagination || {}
    return (clientHeight ?? 0) - (headerHeight ?? 0) - (paginationHeight ?? 0)
  })
  return { containerRef, scrollY }
}
export default useTableHeight

2. 表格里配置分页配置

由于a-table默认带了分页pagination, 也可以 :pagination="false"屏蔽

但是用的时候想配页码页数就要加配置

写成 :pagination="{}"的形式

页码改变调用change回调, 自定义参数后不调用change无法改变页码

 <a-table :pagination="pagination" @change="pageChange"
      :scroll="{ y: 650, x: 'max-content' }" class="mt-[20px]" :columns="service_demand_column" :data-source="tableData">
     ...
    </a-table>
    
// 分页参数
const pagination = reactive({
  current: 1,
  pageSize: 15,
  total: 0
})

3. 表格根据配置生成列

ant 的表格直接支持传入 columns 配置, 不像element需要自己遍历 columns 里也支持 customRender 渲染自定义的jsx, 但如果要绑定事件就需要把那一项单独在vue组件添加了

// 在配置文件columnData.tsx中
export const energy_statistics_column = [
  { title: '能源名称', dataIndex: 'energyName' },
  { title: '所属单位', dataIndex: 'unit' },
  { title: '发电量(kW·h)', dataIndex: 'powerQuantity' },
  { title: '投运日期', dataIndex: 'commissioningDate' },

  {
    title: '运行状态', dataIndex: 'commissioningStatus', align: 'center',
    customRender: ({ text, record, index, column }) => {
      return (
        <Button type="primary" size="small"
          style={{ boxShadow: 'none', background: energy_statistics__list[text][1], color: energy_statistics__list[text][0] }}>{
            text || '-'
          }</Button>
      );
    }
  },
];

// 在使用组件 xx.vue中
    <!-- 表格 -->
    <a-table :pagination="pagination" @change="pageChange" scrollToFirstRowOnChange
      :scroll="{ y: 490, x: 'max-content' }" class="mt-[20px]" :columns="columnData"
      :data-source="tableData">
    </a-table>
   
 <script setup lang='tsx'>
import { energy_statistics_column } from '@/data/columnData';

const columnData = [...energy_statistics_column, {
  title: '操作',
  align: 'center',
  dataIndex: 'operation',
  fixed: "right",
  customRender: ({ text, record, index, column }) => {
    return (
      <a-button type="link" onClick={() => clickDetail(record)}>
        详情
      </a-button>
    );
  },
}]

/**
 * 点击表格详情
 */
const clickDetail = (record) => {
}
</script>

4. watch监听表格筛选项、搜索项、页码页数改变, 统一调用请求表格数据

  1. 给a-table绑定页码页数参数, change更改参数
  2. computed计算请求参数, 清除对象空值
  3. watch监听页码和请求参数, 触发请求函数
  4. 请求表格数据
    <!-- 筛选表单 -->
    <FormFilter class="w-[240px]" :formData="formData">
        <a-button class="mr-[10px]" type="primary">确定</a-button>
        <a-button color="#e7e7e7" @click="resetFilter">重置</a-button>
    </FormFilter>
    <!-- 搜索框 -->
    <a-input style="width: 250px;margin-left: 10px;" placeholder="搜索用户名、用户编号"></a-input>
    <!-- 表格 -->      
    <div ref="refTable" class="flex-1 px-[20px] overflow-hidden">
      <a-table :pagination="pagination" scrollToFirstRowOnChange :scroll="{ y: scrollY  - 40, x: 'max-content' }"
      @change="pageChange" :columns="columnData"
      :data-source="tableData">
    </a-table>
    </div>
    
// 分页参数
const pagination = reactive({
  current: 1,
  pageSize: 15,
  total: 0
})
const keywords = ref('') // 搜索关键词 
const tableData = ref([]) // 表格数据
const formData = ref([...customer_group_filter]); // 表单配置

// 2. computed计算请求参数, 清除对象空值
// 请求参数
const requestParams = computed(() => {
  const obj = {
    keywords: keywords.value
    year: props.year,
    current: pagination.current,
    size: pagination.pageSize,
  };
  formData.value.forEach(({ value, key }) => {
    Object.assign(obj, { [key]: value });
  });
  return cleanObject(obj);
});
/**
 * 清理对象中的空值
 * @param {T} obj
 * @returns {Partial<T>}
 */
export const cleanObject = <T extends {}>(obj?: T): Partial<T> => {
  if (!obj) return {};

  return Object.entries(obj).reduce(
    (tempObj, [key, value]) => ({
      ...tempObj,
      ...(!Object.is(value ?? '', '') && {
        [key]: value,
      }),
    }),
    {} as T,
  );
};

// 3. watch监听页码和请求参数, 触发请求函数
watch([requestParams], () => {
  getCustomerInfo()
}, { deep: true })

// 1. 给a-table绑定页码页数参数, change更改参数
/**
 * 页码改变
 */
const pageChange = (page) => {
  Object.keys(page).forEach(key => {
    pagination[key] = page[key];
  });
}

// 4. 请求表格数据
/**
 * 请求用户数据
 */
const getCustomerInfo = async () => {
  const { code, data } = await getCustomerList(requestParams.value)
  if (code === 2000) {
    tableData.value = data.records
  }
}