diff --git a/frontend/src/assets/style/arco-reset.less b/frontend/src/assets/style/arco-reset.less index 9ef7c93323..24be577520 100644 --- a/frontend/src/assets/style/arco-reset.less +++ b/frontend/src/assets/style/arco-reset.less @@ -150,6 +150,12 @@ } /** 输入框,选择器,文本域 **/ +.arco-select { + .arco-icon { + font-size: 16px; + color: var(--color-text-brand); + } +} .arco-input-wrapper, .arco-textarea-wrapper, .arco-input-tag, @@ -483,3 +489,25 @@ .arco-switch-checked { background: rgb(var(--primary-6)) !important; } + +/** 分页 **/ +.arco-pagination-total { + color: var(--color-text-2) !important; +} +.arco-pagination-options { + margin-left: 0 !important; +} +.arco-pagination-total { + margin-right: 16px !important; +} +.arco-pagination-item-previous { + margin-left: 14px !important; +} +.arco-pagination-size-small .arco-pagination-item { + border: 1px solid var(--color-text-input-border); +} +.arco-pagination-item-active { + border-color: rgb(var(--primary-5)) !important; + color: rgb(var(--primary-5)) !important; + background-color: rgb(var(--primary-1)) !important; +} diff --git a/frontend/src/components/pure/ms-icon-font/index.vue b/frontend/src/components/pure/ms-icon-font/index.vue index b6150e3dd4..0cc9f18f8a 100644 --- a/frontend/src/components/pure/ms-icon-font/index.vue +++ b/frontend/src/components/pure/ms-icon-font/index.vue @@ -17,3 +17,9 @@ extraProps: { ...props }, }); + + diff --git a/frontend/src/components/pure/ms-pagination/index.ts b/frontend/src/components/pure/ms-pagination/index.ts new file mode 100644 index 0000000000..a7025ca010 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue'; +import type { ArcoOptions } from './types'; +import { setGlobalConfig, getComponentPrefix } from './utils'; +import _Pagination from './pagination'; + +const MsPagination = Object.assign(_Pagination, { + install: (app: App, options?: ArcoOptions) => { + setGlobalConfig(app, options); + const componentPrefix = getComponentPrefix(options); + + app.component(componentPrefix + _Pagination.name, _Pagination); + }, +}); + +export type PaginationInstance = InstanceType; +export type { PaginationProps } from './interface'; + +export default MsPagination; diff --git a/frontend/src/components/pure/ms-pagination/interface.ts b/frontend/src/components/pure/ms-pagination/interface.ts new file mode 100644 index 0000000000..2bd05772d2 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/interface.ts @@ -0,0 +1,29 @@ +import { CSSProperties } from 'vue'; +import { Size } from './types'; +import { SelectProps } from '@arco-design/web-vue'; + +export const PAGE_ITEM_TYPES = ['page', 'more', 'previous', 'next'] as const; + +export type PageItemType = (typeof PAGE_ITEM_TYPES)[number]; + +export interface PaginationProps { + total?: number; + current?: number; + defaultCurrent?: number; + pageSize?: number; + defaultPageSize?: number; + disabled?: boolean; + hideOnSinglePage?: boolean; + simple?: boolean; + showTotal?: boolean; + showMore?: boolean; + showJumper?: boolean; + showPageSize?: boolean; + pageSizeOptions?: number[]; + pageSizeProps?: SelectProps; + size?: Size; + pageItemStyle?: CSSProperties; + activePageItemStyle?: CSSProperties; + baseSize?: number; + bufferSize?: number; +} diff --git a/frontend/src/components/pure/ms-pagination/locale/en-US.ts b/frontend/src/components/pure/ms-pagination/locale/en-US.ts new file mode 100644 index 0000000000..af672074b9 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/locale/en-US.ts @@ -0,0 +1,10 @@ +export default { + msPagination: { + total: 'A total of {total} items', + current: 'Current {current}', + pageSize: 'Page size', + goto: 'Goto', + page: 'Page', + countPerPage: ' / Page', + }, +}; diff --git a/frontend/src/components/pure/ms-pagination/locale/zh-CN.ts b/frontend/src/components/pure/ms-pagination/locale/zh-CN.ts new file mode 100644 index 0000000000..9eba5eb25b --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/locale/zh-CN.ts @@ -0,0 +1,10 @@ +export default { + msPagination: { + total: '共 {total} 项数据', + current: '当前页数 {current}', + countPerPage: '条/页', + pageSize: '每页条数', + goto: '前往', + page: '页', + }, +}; diff --git a/frontend/src/components/pure/ms-pagination/page-item-ellipsis.vue b/frontend/src/components/pure/ms-pagination/page-item-ellipsis.vue new file mode 100644 index 0000000000..7219ab9fd3 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/page-item-ellipsis.vue @@ -0,0 +1,58 @@ + + + diff --git a/frontend/src/components/pure/ms-pagination/page-item-step.vue b/frontend/src/components/pure/ms-pagination/page-item-step.vue new file mode 100644 index 0000000000..429d8cc7af --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/page-item-step.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/components/pure/ms-pagination/page-item.vue b/frontend/src/components/pure/ms-pagination/page-item.vue new file mode 100644 index 0000000000..076284a442 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/page-item.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/src/components/pure/ms-pagination/page-jumper.vue b/frontend/src/components/pure/ms-pagination/page-jumper.vue new file mode 100644 index 0000000000..b990edd9cd --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/page-jumper.vue @@ -0,0 +1,103 @@ + + + diff --git a/frontend/src/components/pure/ms-pagination/page-options.vue b/frontend/src/components/pure/ms-pagination/page-options.vue new file mode 100644 index 0000000000..a91d080eff --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/page-options.vue @@ -0,0 +1,51 @@ + + + diff --git a/frontend/src/components/pure/ms-pagination/pagination.tsx b/frontend/src/components/pure/ms-pagination/pagination.tsx new file mode 100644 index 0000000000..fbb7ed1b92 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/pagination.tsx @@ -0,0 +1,428 @@ +import type { PropType, CSSProperties } from 'vue'; +import { computed, defineComponent, reactive, ref, toRefs, watch } from 'vue'; +import { getPrefixCls, isNumber } from './utils'; +import { Size } from './types'; +import Pager from './page-item.vue'; +import StepPager from './page-item-step.vue'; +import EllipsisPager from './page-item-ellipsis.vue'; +import PageJumper from './page-jumper.vue'; +import PageOptions from './page-options.vue'; +import { useI18n } from '@/hooks/useI18n'; +import type { PageItemType } from './interface'; +import { SelectProps } from '@arco-design/web-vue/es/select/interface'; +import useSize from './useSize'; + +export type Data = Record; + +export default defineComponent({ + name: 'MsPagination', + props: { + /** + * @zh 数据总数 + * @en Total number of data + */ + total: { + type: Number, + required: true, + }, + /** + * @zh 当前的页数 + * @en Current page number + * @vModel + */ + current: Number, + /** + * @zh 默认的页数(非受控状态) + * @en The default number of pages (uncontrolled state) + */ + defaultCurrent: { + type: Number, + default: 1, + }, + /** + * @zh 每页展示的数据条数 + * @en Number of data items displayed per page + * @vModel + */ + pageSize: Number, + /** + * @zh 默认每页展示的数据条数(非受控状态) + * @en The number of data items displayed per page by default (uncontrolled state) + */ + defaultPageSize: { + type: Number, + default: 10, + }, + /** + * @zh 是否禁用 + * @en Whether to disable + */ + disabled: { + type: Boolean, + default: false, + }, + /** + * @zh 单页时是否隐藏分页 + * @en Whether to hide pagination when single page + */ + hideOnSinglePage: { + type: Boolean, + default: false, + }, + /** + * @zh 是否为简单模式 + * @en Whether it is simple mode + */ + simple: { + type: Boolean, + default: false, + }, + /** + * @zh 是否显示数据总数 + * @en Whether to display the total number of data + */ + showTotal: { + type: Boolean, + default: false, + }, + /** + * @zh 是否显示更多按钮 + * @en Whether to show more buttons + */ + showMore: { + type: Boolean, + default: false, + }, + /** + * @zh 是否显示跳转 + * @en Whether to show jump + */ + showJumper: { + type: Boolean, + default: false, + }, + /** + * @zh 是否显示数据条数选择器 + * @en Whether to display the data number selector + */ + showPageSize: { + type: Boolean, + default: false, + }, + /** + * @zh 数据条数选择器的选项列表 + * @en Selection list of data number selector + */ + pageSizeOptions: { + type: Array as PropType, + default: () => [10, 20, 30, 40, 50], + }, + /** + * @zh 数据条数选择器的Props + * @en Props of data number selector + */ + pageSizeProps: { + type: Object as PropType, + }, + /** + * @zh 分页选择器的大小 + * @en The size of the page selector + * @values 'mini', 'small', 'medium', 'large' + * @defaultValue 'medium' + */ + size: { + type: String as PropType, + }, + /** + * @zh 分页按钮的样式 + * @en The style of the paging button + */ + pageItemStyle: { + type: Object as PropType, + }, + /** + * @zh 当前分页按钮的样式 + * @en The style of the current paging button + */ + activePageItemStyle: { + type: Object as PropType, + }, + /** + * @zh 计算显示省略的基础个数。显示省略的个数为 `baseSize + 2 * bufferSize` + * @en Calculate and display the number of omitted bases. Display the omitted number as `baseSize + 2 * bufferSize` + */ + baseSize: { + type: Number, + default: 6, + }, + /** + * @zh 显示省略号时,当前页码左右显示的页码个数 + * @en When the ellipsis is displayed, the number of page numbers displayed on the left and right of the current page number + */ + bufferSize: { + type: Number, + default: 2, + }, + /** + * @zh 是否在改变数据条数时调整页码 + * @en Whether to adjust the page number when changing the number of data + * @version 2.34.0 + */ + autoAdjust: { + type: Boolean, + default: true, + }, + }, + emits: { + /* eslint-disable @typescript-eslint/no-unused-vars */ + 'update:current': (current: number) => true, + 'update:pageSize': (pageSize: number) => true, + /** + * @zh 页码改变时触发 + * @en Triggered when page number changes + * @param {number} current + */ + 'change': (current: number) => true, + /** + * @zh 数据条数改变时触发 + * @en Triggered when the number of data items changes + * @param {number} pageSize + */ + 'pageSizeChange': (pageSize: number) => true, + }, + /** + * @zh 分页按钮 + * @en Page item + * @version 2.9.0 + * @slot page-item + * @binding {number} page The page number of the paging button + */ + /** + * @zh 分页按钮(步) + * @en Page item (step) + * @version 2.9.0 + * @slot page-item-step + * @binding {'previous'|'next'} type The type of page item step + */ + /** + * @zh 分页按钮(省略) + * @en Page item (ellipsis) + * @version 2.9.0 + * @slot page-item-ellipsis + */ + /** + * @zh 总数 + * @en Total + * @version 2.9.0 + * @slot total + * @binding {number} total + */ + setup(props, { emit, slots }) { + const prefixCls = getPrefixCls('pagination'); + const { t } = useI18n(); + const { disabled, pageItemStyle, activePageItemStyle, size } = toRefs(props); + const { mergedSize } = useSize(size); + + const _current = ref(props.defaultCurrent); + const _pageSize = ref(props.defaultPageSize); + const computedCurrent = computed(() => props.current ?? _current.value); + const computedPageSize = computed(() => props.pageSize ?? _pageSize.value); + + const pages = computed(() => Math.ceil(props.total / computedPageSize.value)); + + const handleClick = (page: number) => { + // when pageJumper blur and input.value is undefined, page is illegal + if (page !== computedCurrent.value && isNumber(page) && !props.disabled) { + _current.value = page; + emit('update:current', page); + emit('change', page); + } + }; + + const handlePageSizeChange = (pageSize: number) => { + _pageSize.value = pageSize; + emit('update:pageSize', pageSize); + emit('pageSizeChange', pageSize); + }; + + const pagerProps = reactive({ + current: computedCurrent, + pages, + disabled, + style: pageItemStyle, + activeStyle: activePageItemStyle, + onClick: handleClick, + }); + + const getPageItemElement = (type: PageItemType, prop: Data = {}) => { + if (type === 'more') { + return ; + } + if (type === 'previous') { + return ; + } + if (type === 'next') { + return ; + } + + return ; + }; + + const pageList = computed(() => { + const pageListArr: Array = []; + + if (pages.value < props.baseSize + props.bufferSize * 2) { + for (let i = 1; i <= pages.value; i++) { + pageListArr.push(getPageItemElement('page', { key: i, pageNumber: i })); + } + } else { + let left = 1; + let right = pages.value; + let hasLeftEllipsis = false; + let hasRightEllipsis = false; + + if (computedCurrent.value > 2 + props.bufferSize) { + hasLeftEllipsis = true; + left = Math.min(computedCurrent.value - props.bufferSize, pages.value - 2 * props.bufferSize); + } + if (computedCurrent.value < pages.value - (props.bufferSize + 1)) { + hasRightEllipsis = true; + right = Math.max(computedCurrent.value + props.bufferSize, 2 * props.bufferSize + 1); + } + + if (hasLeftEllipsis) { + pageListArr.push(getPageItemElement('page', { key: 1, pageNumber: 1 })); + pageListArr.push( + getPageItemElement('more', { + key: 'left-ellipsis-pager', + step: -(props.bufferSize * 2 + 1), + }) + ); + } + + for (let i = left; i <= right; i++) { + pageListArr.push(getPageItemElement('page', { key: i, pageNumber: i })); + } + + if (hasRightEllipsis) { + pageListArr.push( + getPageItemElement('more', { + key: 'right-ellipsis-pager', + step: props.bufferSize * 2 + 1, + }) + ); + pageListArr.push( + getPageItemElement('page', { + key: pages.value, + pageNumber: pages.value, + }) + ); + } + } + + return pageListArr; + }); + + const renderPager = () => { + if (props.simple) { + return ( + + {getPageItemElement('previous', { simple: true })} + + {getPageItemElement('next', { simple: true })} + + ); + } + + return ( +
    + {getPageItemElement('previous', { simple: true })} + {pageList.value} + {props.showMore && + getPageItemElement('more', { + key: 'more', + step: props.bufferSize * 2 + 1, + })} + {getPageItemElement('next', { simple: true })} +
+ ); + }; + + // When the number of data items changes, recalculate the page number + watch(computedPageSize, (curPageSize, prePageSize) => { + if (props.autoAdjust && curPageSize !== prePageSize && computedCurrent.value > 1) { + const index = prePageSize * (computedCurrent.value - 1) + 1; + const newPage = Math.ceil(index / curPageSize); + if (newPage !== computedCurrent.value) { + _current.value = newPage; + emit('update:current', newPage); + emit('change', newPage); + } + } + }); + + watch(pages, (curPages, prePages) => { + if (props.autoAdjust && curPages !== prePages && computedCurrent.value > 1 && computedCurrent.value > curPages) { + _current.value = curPages; + emit('update:current', curPages); + emit('change', curPages); + } + }); + + const cls = computed(() => [ + prefixCls, + `${prefixCls}-size-${mergedSize.value}`, + { + [`${prefixCls}-simple`]: props.simple, + [`${prefixCls}-disabled`]: props.disabled, + }, + ]); + + return () => { + if (props.hideOnSinglePage && pages.value <= 1) { + return null; + } + + return ( +
+ {props.showTotal && ( + + {slots.total?.({ total: props.total }) ?? t('msPagination.total', { total: props.total })} + + )} + {props.showPageSize && ( + handlePageSizeChange(v)} + selectProps={props.pageSizeProps} + /> + )} + {renderPager()} + {!props.simple && props.showJumper && ( + + )} +
+ ); + }; + }, +}); diff --git a/frontend/src/components/pure/ms-pagination/types.ts b/frontend/src/components/pure/ms-pagination/types.ts new file mode 100644 index 0000000000..0b2605af1a --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/types.ts @@ -0,0 +1,20 @@ +import { Slots } from 'vue'; +import { ArcoLang } from '@arco-design/web-vue/es/locale/interface'; + +export interface ArcoOptions { + classPrefix?: string; + componentPrefix?: string; +} +export const SIZES = ['mini', 'small', 'medium', 'large'] as const; + +export type Size = (typeof SIZES)[number]; + +export interface ConfigProvider { + slots: Slots; + prefixCls?: string; + locale?: ArcoLang; + size?: Size; + updateAtScroll?: boolean; + scrollToClose?: boolean; + exchangeTime?: boolean; +} diff --git a/frontend/src/components/pure/ms-pagination/useSize.ts b/frontend/src/components/pure/ms-pagination/useSize.ts new file mode 100644 index 0000000000..7cd68fa31b --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/useSize.ts @@ -0,0 +1,15 @@ +import { computed, inject, Ref } from 'vue'; +import { Size } from './types'; +import { configProviderInjectionKey } from './utils'; + +const useSize = (size?: Ref, { defaultValue = 'medium' }: { defaultValue?: Size } = {}) => { + const configProviderCtx = inject(configProviderInjectionKey, undefined); + + const mergedSize = computed(() => size?.value ?? configProviderCtx?.size ?? defaultValue); + + return { + mergedSize, + }; +}; + +export default useSize; diff --git a/frontend/src/components/pure/ms-pagination/utils.ts b/frontend/src/components/pure/ms-pagination/utils.ts new file mode 100644 index 0000000000..4579a2f722 --- /dev/null +++ b/frontend/src/components/pure/ms-pagination/utils.ts @@ -0,0 +1,50 @@ +import type { App } from 'vue'; +import { getCurrentInstance, inject, InjectionKey } from 'vue'; +import type { ArcoOptions, ConfigProvider } from './types'; + +const COMPONENT_PREFIX = 'A'; +const CLASS_PREFIX = 'arco'; +const GLOBAL_CONFIG_NAME = '$arco'; + +export const configProviderInjectionKey: InjectionKey = Symbol('ArcoConfigProvider'); + +export const getComponentPrefix = (options?: ArcoOptions) => { + return options?.componentPrefix ?? COMPONENT_PREFIX; +}; + +export const setGlobalConfig = (app: App, options?: ArcoOptions): void => { + if (options && options.classPrefix) { + app.config.globalProperties[GLOBAL_CONFIG_NAME] = { + ...(app.config.globalProperties[GLOBAL_CONFIG_NAME] ?? {}), + classPrefix: options.classPrefix, + }; + } +}; + +export const getPrefixCls = (componentName?: string): string => { + const instance = getCurrentInstance(); + const configProvider = inject(configProviderInjectionKey, undefined); + + const prefix = + configProvider?.prefixCls ?? + instance?.appContext.config.globalProperties[GLOBAL_CONFIG_NAME]?.classPrefix ?? + CLASS_PREFIX; + if (componentName) { + return `${prefix}-${componentName}`; + } + return prefix; +}; + +export const getLegalPage = (page: number, { min, max }: { min: number; max: number }): number => { + if (page < min) { + return min; + } + if (page > max) { + return max; + } + return page; +}; + +export function isNumber(obj: any): obj is number { + return Object.prototype.toString.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line +} diff --git a/frontend/src/components/pure/ms-table/base-table.vue b/frontend/src/components/pure/ms-table/base-table.vue index 111da57cb2..13ce28adcb 100644 --- a/frontend/src/components/pure/ms-table/base-table.vue +++ b/frontend/src/components/pure/ms-table/base-table.vue @@ -75,7 +75,7 @@ } from './type'; import BatchAction from './batchAction.vue'; - import type { TableColumnData, TableData } from '@arco-design/web-vue'; + import type { TableData } from '@arco-design/web-vue'; import ColumnSelector from './columnSelector.vue'; const batchleft = ref('10px'); diff --git a/frontend/src/views/bug-management/index.vue b/frontend/src/views/bug-management/index.vue index 7847331e08..05ec5c2285 100644 --- a/frontend/src/views/bug-management/index.vue +++ b/frontend/src/views/bug-management/index.vue @@ -1,3 +1,31 @@ - + - + + +