feat: batch table

This commit is contained in:
RubyLiu 2023-06-08 14:28:53 +08:00 committed by rubylliu
parent d8d94a0c20
commit 51ac0e2f97
126 changed files with 544 additions and 1797 deletions

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
import Footer from '@/components/footer/index.vue';
import Footer from '@/components/pure/footer/index.vue';
describe('Footer', () => {
test('renders the correct text', () => {

View File

@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
import MsBaseTable from '@/components/ms-table/base-table.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { nextTick } from 'vue';
import { MsTableColumn } from '@/components/ms-table/type';
import useTable from '@/components/ms-table/useTable';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getTableList } from '@/api/modules/api-test/index';
const columns: MsTableColumn = [
@ -71,7 +71,6 @@ describe('MS-Table', () => {
test('init table with useTable', async () => {
const { propsRes, propsEvent, loadList, setProps } = useTable(getTableList, {
columns,
pagination: { current: 1, pageSize: 1 },
});
const wrapper = mount(MsBaseTable, {
@ -85,7 +84,7 @@ describe('MS-Table', () => {
expect(propsRes.value.data.length).toBe(2);
expect(content).toBe('e7bd7179-d63a-43a5-1a65-218473ee69ca');
setProps({ pagination: { current: 2, pageSize: 1 } });
setProps({});
loadList();
await nextTick();

View File

@ -5,57 +5,57 @@
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {};
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAffix: typeof import('@arco-design/web-vue')['Affix'];
AAlert: typeof import('@arco-design/web-vue')['Alert'];
AAvatar: typeof import('@arco-design/web-vue')['Avatar'];
ABadge: typeof import('@arco-design/web-vue')['Badge'];
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'];
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'];
AButton: typeof import('@arco-design/web-vue')['Button'];
ACard: typeof import('@arco-design/web-vue')['Card'];
ACardMeta: typeof import('@arco-design/web-vue')['CardMeta'];
ACol: typeof import('@arco-design/web-vue')['Col'];
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'];
ADivider: typeof import('@arco-design/web-vue')['Divider'];
ADoption: typeof import('@arco-design/web-vue')['Doption'];
ADrawer: typeof import('@arco-design/web-vue')['Drawer'];
ADropdown: typeof import('@arco-design/web-vue')['Dropdown'];
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'];
ALayout: typeof import('@arco-design/web-vue')['Layout'];
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent'];
ALayoutFooter: typeof import('@arco-design/web-vue')['LayoutFooter'];
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'];
ALink: typeof import('@arco-design/web-vue')['Link'];
AList: typeof import('@arco-design/web-vue')['List'];
AListItem: typeof import('@arco-design/web-vue')['ListItem'];
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta'];
AMenu: typeof import('@arco-design/web-vue')['Menu'];
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'];
AModal: typeof import('@arco-design/web-vue')['Modal'];
AOption: typeof import('@arco-design/web-vue')['Option'];
APagination: typeof import('@arco-design/web-vue')['Pagination'];
APopover: typeof import('@arco-design/web-vue')['Popover'];
AResult: typeof import('@arco-design/web-vue')['Result'];
ARow: typeof import('@arco-design/web-vue')['Row'];
ASelect: typeof import('@arco-design/web-vue')['Select'];
ASkeleton: typeof import('@arco-design/web-vue')['Skeleton'];
ASkeletonLine: typeof import('@arco-design/web-vue')['SkeletonLine'];
ASkeletonShape: typeof import('@arco-design/web-vue')['SkeletonShape'];
ASpace: typeof import('@arco-design/web-vue')['Space'];
ASpin: typeof import('@arco-design/web-vue')['Spin'];
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu'];
ASwitch: typeof import('@arco-design/web-vue')['Switch'];
ATabPane: typeof import('@arco-design/web-vue')['TabPane'];
ATabs: typeof import('@arco-design/web-vue')['Tabs'];
ATag: typeof import('@arco-design/web-vue')['Tag'];
ATooltip: typeof import('@arco-design/web-vue')['Tooltip'];
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph'];
ATypographyText: typeof import('@arco-design/web-vue')['TypographyText'];
RouterLink: typeof import('vue-router')['RouterLink'];
RouterView: typeof import('vue-router')['RouterView'];
AAffix: typeof import('@arco-design/web-vue')['Affix']
AAlert: typeof import('@arco-design/web-vue')['Alert']
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
ABadge: typeof import('@arco-design/web-vue')['Badge']
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button']
ACard: typeof import('@arco-design/web-vue')['Card']
ACardMeta: typeof import('@arco-design/web-vue')['CardMeta']
ACol: typeof import('@arco-design/web-vue')['Col']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
ALayoutFooter: typeof import('@arco-design/web-vue')['LayoutFooter']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link']
AList: typeof import('@arco-design/web-vue')['List']
AListItem: typeof import('@arco-design/web-vue')['ListItem']
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta']
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
AOption: typeof import('@arco-design/web-vue')['Option']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
AResult: typeof import('@arco-design/web-vue')['Result']
ARow: typeof import('@arco-design/web-vue')['Row']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASkeleton: typeof import('@arco-design/web-vue')['Skeleton']
ASkeletonLine: typeof import('@arco-design/web-vue')['SkeletonLine']
ASkeletonShape: typeof import('@arco-design/web-vue')['SkeletonShape']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATypographyParagraph: typeof import('@arco-design/web-vue')['TypographyParagraph']
ATypographyText: typeof import('@arco-design/web-vue')['TypographyText']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@ -2,7 +2,6 @@
<a-config-provider :locale="locale">
<router-view />
<global-setting />
<ThemeBox />
</a-config-provider>
</template>
@ -10,9 +9,8 @@
import { computed } from 'vue';
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
import GlobalSetting from '@/components/global-setting/index.vue';
import GlobalSetting from '@/components/pure/global-setting/index.vue';
import useLocale from '@/locale/useLocale';
import ThemeBox from '@/components/theme-box/index.vue';
const { currentLocale } = useLocale();
const locale = computed(() => {

View File

@ -1,13 +1,13 @@
import MSR from '@/api/http/index';
import { GetApiTestList, GetApiTestListUrl } from '@/api/requrls/api-test';
import { QueryParams } from '@/components/ms-table/useTable';
import { QueryParams } from '@/models/common';
import { ApiTestListI } from '@/models/api-test';
export function getTableList(params: QueryParams) {
const { current, pageSize } = params;
const { current, pageSize, sort, filter, keyword } = params;
return MSR.post<ApiTestListI>({
url: GetApiTestList,
data: { current, pageSize, projectId: 'test-project-id' },
data: { current, pageSize, sort, filter, keyword, projectId: 'test-project-id' },
});
}

View File

@ -1,15 +0,0 @@
<template>
<div class="ms-base-tale">
<a-table v-bind="$attrs">
<template v-for="(item, key, i) in slots" :key="i" #[key]="{ record, rowIndex, column }">
<slot :name="key" v-bind="{ rowIndex, record, column }"></slot>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { useSlots } from 'vue';
const slots = useSlots();
</script>

View File

@ -1,3 +0,0 @@
export default {
msTable: {},
};

View File

@ -1,3 +0,0 @@
export default {
msTable: {},
};

View File

@ -0,0 +1,134 @@
<template>
<div class="ms-base-tale">
<select-all
v-if="attrs.showSelectAll"
class="custom-action"
:total="selectTotal"
:current="selectCurrent"
@change="handleChange"
/>
<a-table v-bind="$attrs" :selected-keys="props.selectedKeys" @selection-change="(e) => selectionChange(e, true)">
<template v-for="(item, key, i) in slots" :key="i" #[key]="{ record, rowIndex, column }">
<slot :name="key" v-bind="{ rowIndex, record, column }"></slot>
</template>
<template v-if="selectCurrent > 0" #footer>
<batch-action
:select-row-count="selectCurrent"
@batch-export="emit('batchExport')"
@batch-edit="emit('batchEdit')"
@batch-move="emit('batchMove')"
@batch-related="emit('batchRelated')"
@batch-delete="emit('batchDelete')"
/>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
// eslint-disable no-console
import { useSlots, useAttrs, computed, ref, onMounted, watchEffect } from 'vue';
import selectAll from './select-all.vue';
import { MsTabelProps, SelectAllEnum, MsPaginationI } from './type';
import BatchAction from './batchAction.vue';
const batchleft = ref('10px');
const props = defineProps({
selectedKeys: {
type: Array as unknown as () => (string | number)[],
default: () => [],
},
});
const emit = defineEmits<{
(e: 'selectedChange', value: (string | number)[]): void;
(e: 'batchExport'): void;
(e: 'batchEdit'): void;
(e: 'batchMove'): void;
(e: 'batchRelated'): void;
(e: 'batchDelete'): void;
}>();
const isSelectAll = ref(false);
// -
const selectCurrent = ref(0);
const slots = useSlots();
const attrs = useAttrs();
const { data, rowKey, pagination }: Partial<MsTabelProps> = attrs;
// -
const selectTotal = computed(() => {
if (pagination) {
const { pageSize } = pagination as MsPaginationI;
return pageSize;
}
return data ? data.length : 20;
});
// change
const selectionChange = (arr: (string | number)[], setCurrentSelect: boolean) => {
emit('selectedChange', arr);
if (setCurrentSelect) {
selectCurrent.value = arr.length;
}
};
// change
const handleChange = (v: string) => {
isSelectAll.value = v === SelectAllEnum.ALL;
if (v === SelectAllEnum.NONE) {
selectionChange([], true);
}
if (v === SelectAllEnum.CURRENT) {
if (data && data.length > 0) {
selectionChange(
data.map((item: any) => item[rowKey || 'id']),
true
);
}
}
if (v === SelectAllEnum.ALL) {
const { total } = pagination as MsPaginationI;
if (data && data.length > 0) {
selectionChange(
data.map((item: any) => item[rowKey || 'id']),
false
);
selectCurrent.value = total;
}
}
};
//
const getBatchLeft = () => {
if (attrs.enableDrag) {
return '30px';
}
switch (attrs.size) {
case 'small':
return '10px';
case 'mini':
return '10px';
default:
return '10px';
}
};
onMounted(() => {
batchleft.value = getBatchLeft();
});
</script>
<style lang="less" scoped>
.ms-base-tale {
position: relative;
.custom-action {
position: absolute;
top: 3px;
left: v-bind(batchleft);
z-index: 100;
border-radius: 2px;
line-height: 40px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div class="ms-table__patch-action">
<span>{{ t('msTable.batch.selected', { count: props.selectRowCount }) }}</span>
<a-button @click="emit('batchExport')">{{ t('msTable.batch.export') }}</a-button>
<a-button @click="emit('batchEdit')">{{ t('msTable.batch.edit') }}</a-button>
<a-button @click="emit('batchExport')">{{ t('msTable.batch.moveTo') }}</a-button>
<a-button @click="emit('batchExport')">{{ t('msTable.batch.copyTo') }}</a-button>
<a-button @click="emit('batchExport')">{{ t('msTable.batch.related') }}</a-button>
<a-button @click="emit('batchExport')">{{ t('msTable.batch.delete') }}</a-button>
<a-button @click="emit('clear')">{{ t('msTable.batch.clear') }}</a-button>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps({
selectRowCount: {
type: Number,
},
});
const emit = defineEmits<{
(e: 'batchExport'): void;
(e: 'batchEdit'): void;
(e: 'batchMove'): void;
(e: 'batchRelated'): void;
(e: 'batchDelete'): void;
(e: 'clear'): void;
}>();
</script>

View File

@ -0,0 +1,19 @@
export default {
msTable: {
current: 'Select Current Page',
all: 'Select All Pages',
batch: {
title: '批量操作',
selected: '已选择 {count} 项',
export: '导出',
edit: '编辑',
delete: '删除',
moveTo: '移动到',
copyTo: '复制到',
related: '关联需求',
generate: '生成依赖关系',
add: '添加到公用用例库',
clear: 'clear',
},
},
};

View File

@ -0,0 +1,19 @@
export default {
msTable: {
current: '全选当前页',
all: '全选所有页',
batch: {
title: '批量操作',
selected: '已选择 {count} 项',
export: '导出',
edit: '编辑',
delete: '删除',
moveTo: '移动到',
copyTo: '复制到',
related: '关联需求',
generate: '生成依赖关系',
add: '添加到公用用例库',
clear: '清空',
},
},
};

View File

@ -0,0 +1,75 @@
<template>
<div class="ms-table-select-all items-center text-base">
<a-checkbox v-model="checked" class="text-base" :indeterminate="indeterminate" @change="handleCheckChange" />
<a-dropdown position="bl" @select="handleSelect">
<a-icon-down class="dropdown-icon ml-0.5" />
<template #content>
<a-doption :value="SelectAllEnum.CURRENT">{{ t('msTable.current') }}</a-doption>
<a-doption :value="SelectAllEnum.ALL">{{ t('msTable.all') }}</a-doption>
</template>
</a-dropdown>
</div>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { SelectAllEnum } from './type';
const { t } = useI18n();
const emit = defineEmits<{
(e: 'change', value: string): void;
}>();
const props = defineProps({
current: {
type: Number,
default: 0,
},
total: {
type: Number,
default: 0,
},
});
const checked = ref(false);
const indeterminate = ref(false);
watchEffect(() => {
if (props.current === 0) {
checked.value = false;
indeterminate.value = false;
} else if (props.current < props.total) {
checked.value = false;
indeterminate.value = true;
} else if (props.current >= props.total) {
checked.value = true;
indeterminate.value = false;
}
});
const handleSelect = (v: string | number | Record<string, any> | undefined) => {
emit('change', v as string);
};
const handleCheckChange = () => {
if (checked.value) {
handleSelect(SelectAllEnum.CURRENT);
} else {
handleSelect(SelectAllEnum.NONE);
}
};
</script>
<style lang="less" scoped>
.ms-table-select-all {
.dropdown-icon {
color: rgb(var(--primary-6));
}
.dropdown-icon:hover {
border-radius: 50%;
background-color: rgb(var(--primary-3));
}
}
</style>

View File

@ -1,13 +1,14 @@
import { TableColumnData, TableData, TableDraggable } from '@arco-design/web-vue';
import { TableColumnData, TableData, TableDraggable, TableChangeExtra } from '@arco-design/web-vue';
export interface MsPaginationI {
pageSize?: number;
total?: number;
current?: number;
current: number;
pageSize: number;
total: number;
showPageSize: boolean;
}
// 表格属性
export interface MsTabelProps extends MsPaginationI {
export interface MsTabelProps {
// 表格列 - 详见 TableColumn https://arco.design/web-vue/components/table-column;
columns: TableColumnData[];
// 表格数据 - 详见 TableData https://arco.design/web-vue/components/table-data;
@ -20,6 +21,7 @@ export interface MsTabelProps extends MsPaginationI {
y?: number | string;
};
// 表格是否可拖拽
enableDrag?: boolean;
draggable?: TableDraggable;
// 表格是否可编辑
editable?: boolean;
@ -29,6 +31,8 @@ export interface MsTabelProps extends MsPaginationI {
sortable?: boolean;
// 表格是否可选中
selectable?: boolean;
// 展示自定义全选
showSelectAll?: boolean;
// 表格是否可展开
expandable?: boolean;
// 表格是否可固定表头
@ -41,9 +45,28 @@ export interface MsTabelProps extends MsPaginationI {
loading?: boolean;
bordered?: boolean;
// pagination
pagination?: MsPaginationI;
pagination: MsPaginationI | boolean;
[key: string]: any;
}
export interface MsTableSelectAll {
value: boolean;
total: number;
current: number;
type: 'all' | 'page';
}
export type MsTableData = TableData[];
export type MsTableColumn = TableColumnData[];
export type MSTableChangeExtra = TableChangeExtra;
// eslint-disable-next-line no-shadow
export enum SelectAllEnum {
ALL = 'all',
CURRENT = 'current',
NONE = 'none',
}
export interface SortItem {
[key: string]: string;
}

View File

@ -1,9 +1,12 @@
// 核心的封装方法,详细参数看文档 https://arco.design/vue/component/table
// hook/table-props.ts
import { ref, reactive } from 'vue';
import { ref } from 'vue';
import { MsTabelProps, MsTableData, MsTableColumn } from './type';
import { ApiTestListI } from '@/models/api-test';
import { TableData } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import { QueryParams } from '@/models/common';
export interface Pagination {
current: number;
@ -12,47 +15,62 @@ export interface Pagination {
showPageSize: boolean;
}
export interface QueryParams {
current: number;
pageSize: number;
[key: string]: any;
}
type GetListFunc = (v: QueryParams) => Promise<ApiTestListI>;
export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<MsTabelProps>) {
// 行选择
const rowSelection = reactive({
const rowSelection = {
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: false,
});
showCheckedAll: false,
};
const defaultProps: MsTabelProps = {
'bordered': true,
'size': 'small',
'scroll': { y: '550px', x: '1400px' },
'checkable': true,
'expandable': false,
'loading': true,
'data': [] as MsTableData,
'columns': [] as MsTableColumn,
'pagination': {
bordered: true,
showPagination: true,
size: 'small',
scroll: { y: '550px', x: '1400px' },
checkable: true,
loading: true,
data: [] as MsTableData,
columns: [] as MsTableColumn,
pagination: {
current: 1,
pageSize: 20,
total: 0,
showPageSize: true,
} as Pagination,
'draggable': { type: 'handle' },
'row-key': 'id',
rowKey: 'id',
selectedKeys: [],
selectedAll: false,
enableDrag: false,
showSelectAll: true,
...props,
};
// 属性组
const propsRes = ref(defaultProps);
// 排序
const sortItem = ref<object>({});
// 筛选
const filterItem = ref<object>({});
// keyword
const keyword = ref('');
// 是否分页
if (!propsRes.value.showPagination) {
propsRes.value.pagination = false;
}
// 是否可选中
if (propsRes.value.selectable) {
propsRes.value['row-selection'] = rowSelection;
propsRes.value.rowSelection = rowSelection;
}
// 是否可拖拽
if (propsRes.value.enableDrag) {
propsRes.value.draggable = { type: 'handle' };
}
// 加载效果
@ -72,9 +90,11 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
}
const setPagination = ({ current, total }: SetPaginationPrams) => {
if (propsRes.value.pagination) {
if (propsRes.value.pagination && typeof propsRes.value.pagination === 'object') {
propsRes.value.pagination.current = current;
if (total) propsRes.value.pagination.total = total;
if (total) {
propsRes.value.pagination.total = total;
}
}
};
@ -93,6 +113,10 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
loadListParams.value = params || {};
};
const setKeyword = (v: string) => {
keyword.value = v;
};
// 加载分页列表数据
const loadList = async () => {
const { current, pageSize } = propsRes.value.pagination as Pagination;
@ -100,9 +124,20 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
const data = await loadListFunc({
current,
pageSize,
...loadListParams.value,
sort: sortItem.value,
filter: filterItem.value,
keyword: keyword.value,
});
const tmpArr = data.list as unknown as MsTableData;
propsRes.value.data = tmpArr.map((item: TableData) => {
if (item.updateTime) {
item.updateTime = dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss');
}
if (item.createTime) {
item.createTime = dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss');
}
return item;
});
propsRes.value.data = data.list as unknown as MsTableData;
setPagination({ current: data.current, total: data.total });
setLoading(false);
return data;
@ -113,7 +148,14 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
// 排序触发
sorterChange: (dataIndex: string, direction: string) => {
// eslint-disable-next-line no-console
console.log(dataIndex, direction);
sortItem.value = { [dataIndex]: direction };
loadList();
},
// 筛选触发
filterChange: (dataIndex: string, filteredValues: string[]) => {
filterItem.value = { [dataIndex]: filteredValues };
loadList();
},
// 分页触发
pageChange: (current: number) => {
@ -122,13 +164,23 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
},
// 修改每页显示条数
pageSizeChange: (pageSize: number) => {
if (propsRes.value.pagination) {
if (propsRes.value.pagination && typeof propsRes.value.pagination === 'object') {
propsRes.value.pagination.pageSize = pageSize;
}
loadList();
},
// 选择触发
selectedChange: (arr: (string | number)[]) => {
if (arr.length === 0) {
propsRes.value.pagination = defaultProps.pagination;
} else {
propsRes.value.pagination = false;
}
propsRes.value.selectedKeys = arr;
},
change: (_data: MsTableData) => {
if (propsRes.value.draggable) {
if (propsRes.value.draggable && _data instanceof Array) {
// eslint-disable-next-line vue/require-explicit-emits
propsRes.value.data = _data;
}
@ -143,5 +195,6 @@ export default function useTbleProps(loadListFunc: GetListFunc, props?: Partial<
loadList,
setPagination,
setLoadListParams,
setKeyword,
};
}

View File

@ -142,9 +142,9 @@
import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/locale/useLocale';
import useUser from '@/hooks/useUser';
import Menu from '@/components/menu/index.vue';
import Menu from '@/components/pure/menu/index.vue';
import MessageBox from '../message-box/index.vue';
import ProjcetSelection from '@/components/project-selection/index.vue';
import ProjcetSelection from '@/components/pure/project-selection/index.vue';
const appStore = useAppStore();
const userStore = useUserStore();

View File

@ -1,275 +0,0 @@
<template>
<a-badge class="theme-badge bottom-[124px] right-[70px]" :count="theme ? 1 : 0" dot>
<a-button
class="theme-badge-button"
:shape="hover ? 'round' : 'circle'"
size="large"
@click="modalVisible = true"
@mouseenter="hover = true"
@mouseleave="hover = false"
>
<IconSkin />
<span v-if="hover" style="margin-left: 8px">
{{ t('themeBox.installTheme') }}
</span>
</a-button>
</a-badge>
<a-modal :visible="modalVisible" :width="900" @ok="modalVisible = false" @cancel="modalVisible = false">
<template #title>
<div class="theme-box-header pr-[24px]">
<span>{{ t('themeBox.installTheme') }}</span>
</div>
</template>
<a-row :gutter="[20, 20]">
<template v-if="isLoading">
<a-col v-for="(_, index) of loadingFillArray" :key="index" :span="8">
<a-card v-if="isLoading" class="theme-box-card">
<template #cover>
<a-skeleton animation>
<a-skeleton-shape style="width: 272px; height: 160px" />
</a-skeleton>
</template>
<a-card-meta>
<template #title>
<a-skeleton animation>
<a-skeleton-line :line-height="25" />
</a-skeleton>
</template>
</a-card-meta>
<a-skeleton animation>
<a-skeleton-shape style="margin-top: 20px; margin-left: auto; width: 100px; height: 24px" />
</a-skeleton>
</a-card>
</a-col>
</template>
<template v-else-if="themeList.length > 0">
<a-col v-for="item of themeList" :key="item.themeId" :span="8">
<a-card class="theme-box-card">
<template #cover>
<img :src="item.cover" style="height: 160px" alt="theme-cover" />
</template>
<template #actions>
<a-button
class="theme-box-card-link"
type="text"
size="mini"
:href="`https://arco.design/themes/design/${item.themeId}`"
>
<template #icon>
<IconLink />
</template>
{{ t('themeBox.openInDesignLab') }}
</a-button>
<a-tag v-if="theme && theme.themeId === item.themeId" color="arcoblue">
{{ t('themeBox.currentTheme') }}
</a-tag>
<a-button v-else type="primary" size="mini" @click="() => useTheme(item)">
{{ t('themeBox.install') }}
</a-button>
</template>
<a-card-meta :title="item.themeName" />
</a-card>
</a-col>
</template>
<template v-else>
<Empty style="margin: 200px 0">
<template #description>
{{ t('themeBox.noResult') }}
<a-link :href="`https://arco.design/themes`">
{{ t('themeBox.createTheme') }}
</a-link>
</template>
</Empty>
</template>
</a-row>
<div class="theme-box-bottom mt-[20px]">
<a-pagination :total="total" :current="page" :page-size="6" @change="onPageChange" />
</div>
<template v-if="theme" #footer>
<div class="theme-box-footer">
<a-typography-text bold> {{ t('themeBox.currentTheme') }}: {{ theme.themeName }} </a-typography-text>
<a-button type="primary" status="danger" size="small" @click="onResetClick">
{{ t('themeBox.resetTheme') }}
</a-button>
</div>
</template>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Notification } from '@arco-design/web-vue';
import axios from 'axios';
import { IconSkin, IconLink } from '@arco-design/web-vue/es/icon';
import { getLocalStorage, removeLocalStorage, setLocalStorage } from '../../utils/local-storage';
import { apiBasename } from '../../utils/api';
import { ThemeData } from './interface';
const THEME_LINK_ID = 'arco-custom-theme';
const loadingFillArray = Array(3).fill(1);
export default defineComponent({
name: 'ThemeBox',
components: {
IconSkin,
IconLink,
},
setup() {
const { t } = useI18n();
const theme = ref<ThemeData>();
const themeList = ref<ThemeData[]>([]);
const total = ref(0);
const page = ref(1);
const modalVisible = ref(false);
const searchValue = ref('');
const isLoading = ref(false);
const hover = ref(false);
const removeTheme = () => {
const linkElement = document.getElementById(THEME_LINK_ID);
if (linkElement) {
document.body.removeChild(linkElement);
}
theme.value = undefined;
};
const addTheme = (_theme: ThemeData, notice: boolean) => {
const url = `${_theme.unpkgHost}${_theme.packageName}/css/arco.css`;
axios
.get(url)
.then(() => {
if (theme.value) {
removeTheme();
}
const linkElement = document.createElement('link');
linkElement.id = THEME_LINK_ID;
linkElement.href = url;
linkElement.type = 'text/css';
linkElement.rel = 'stylesheet';
document.body.appendChild(linkElement);
theme.value = _theme;
if (notice) {
Notification.success({
id: 'theme',
title: t('themeBox.installTheme'),
content: t('themeBox.installThemeSuccess'),
duration: 2000,
});
}
})
.catch(() => {
Notification.error({
id: 'theme',
title: t('themeBox.installTheme'),
content: t('themeBox.installThemeError'),
duration: 2000,
});
});
};
const useTheme = (_theme: ThemeData, notice = true) => {
addTheme(_theme, notice);
setLocalStorage('vue-custom-theme', _theme);
};
onMounted(() => {
// const _theme = getLocalStorage<ThemeData>('vue-custom-theme', true) as ThemeData;
// if (_theme) {
// useTheme(_theme, false);
// }
});
const fetchThemeList = async (current: number, search: string) => {
isLoading.value = true;
try {
const data = await axios.get(
`${apiBasename}/themes/api/open/themes/list?pageSize=6&currentPage=${current}&depLibrary=@arco-design/web-vue&keyword=vue-ms-theme-${search}`
);
themeList.value = data.data.list;
total.value = data.data.total;
} catch (e) {
themeList.value = [];
total.value = 0;
}
isLoading.value = false;
};
watch(modalVisible, (visible) => {
if (visible) {
fetchThemeList(page.value, searchValue.value);
}
});
const onResetClick = () => {
removeTheme();
removeLocalStorage('vue-custom-theme');
};
const onSearchInput = (value: string) => {
searchValue.value = value;
page.value = 1;
fetchThemeList(1, value);
};
const onPageChange = (_page: number) => {
page.value = _page;
fetchThemeList(_page, searchValue.value);
};
return {
hover,
modalVisible,
themeList,
theme,
total,
page,
isLoading,
loadingFillArray,
searchValue,
useTheme,
onSearchInput,
onPageChange,
onResetClick,
t,
};
},
});
</script>
<style scoped lang="less">
.theme-box {
&-header {
@apply flex w-full items-center justify-between;
}
&-card {
&-link {
@apply opacity-0;
transition: opacity 100ms;
}
&:hover &-link {
@apply opacity-100;
}
:deep(.arco-card-meta-title) {
line-height: 25px;
}
}
&-bottom {
@apply flex justify-end;
}
&-footer {
@apply flex items-center justify-between;
}
}
.theme-badge {
@apply fixed;
&-button {
border: 1px solid var(--color-fill-3) !important;
background: var(--color-bg-5) !important;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
}
</style>

View File

@ -1,7 +0,0 @@
export interface ThemeData {
themeId: number;
themeName: string;
cover: string;
packageName: string;
unpkgHost: string;
}

View File

@ -1,17 +0,0 @@
export default {
themeBox: {
currentTheme: 'Current theme',
autoUseTheme: 'Automatically use theme',
install: 'Install',
installTheme: 'Install theme',
search: 'Search',
installingTheme: 'Installing theme...',
installThemeSuccess: 'Install theme successfully! ',
installThemeError: 'Install theme failed, please try again! ',
resetTheme: 'Reset theme',
resetThemeSuccess: 'Reset theme successfully! ',
openInDesignLab: 'Open in the Design Lab',
noResult: 'No related themes',
createTheme: 'Go to the Design Lab to create',
},
};

View File

@ -1,17 +0,0 @@
export default {
themeBox: {
currentTheme: '当前主题',
autoUseTheme: '自动应用主题',
install: '安装',
installTheme: '安装主题',
search: '搜索',
installingTheme: '正在安装主题...',
installThemeSuccess: '主题安装成功!',
installThemeError: '主题安装失败,请重试!',
resetTheme: '重置主题',
resetThemeSuccess: '重置主题成功!',
openInDesignLab: '在主题商店打开',
noResult: '没有相关主题',
createTheme: '前往主题商店创建',
},
};

View File

@ -48,10 +48,10 @@
import { ref, computed, watch, provide, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAppStore, useUserStore } from '@/store';
import NavBar from '@/components/navbar/index.vue';
import Menu from '@/components/menu/index.vue';
import Footer from '@/components/footer/index.vue';
import TabBar from '@/components/tab-bar/index.vue';
import NavBar from '@/components/pure/navbar/index.vue';
import Menu from '@/components/pure/menu/index.vue';
import Footer from '@/components/pure/footer/index.vue';
import TabBar from '@/components/pure/tab-bar/index.vue';
import usePermission from '@/hooks/usePermission';
import useResponsive from '@/hooks/useResponsive';
import PageLayout from './page-layout.vue';

View File

@ -1,38 +1,22 @@
import dayjsLocale from 'dayjs/locale/en';
import localeSettings from './settings';
import sys from './sys';
import localeMessageBox from '@/components/message-box/locale/en-US';
import minder from '@/components/minder-editor/locale/en-US';
import localeMessageBox from '@/components/pure/message-box/locale/en-US';
import minder from '@/components/pure/minder-editor/locale/en-US';
import localeLogin from '@/views/login/locale/en-US';
import localeWorkplace from '@/views/dashboard/workplace/locale/en-US';
import localeThemebox from '@/components/theme-box/locale/en-US';
import localeTable from '@/components/pure/ms-table/locale/en-US';
import localeApiTest from '@/views/api-test/locale/en-US';
export default {
message: {
'menu.component': 'component hub',
'menu.component.demo': 'component demo',
'menu.apitest': 'Api Test',
'menu.dashboard': 'Dashboard',
'menu.minder': 'Minder',
'menu.server.dashboard': 'Dashboard-Server',
'menu.server.workplace': 'Workplace-Server',
'menu.server.monitor': 'Monitor-Server',
'menu.list': 'List',
'menu.result': 'Result',
'menu.exception': 'Exception',
'menu.form': 'Form',
'menu.profile': 'Profile',
'menu.visualization': 'Data Visualization',
'menu.user': 'User Center',
'menu.apiTest': 'Api Test',
'navbar.action.locale': 'Switch to English',
...sys,
...localeSettings,
...localeMessageBox,
...localeLogin,
...localeWorkplace,
...minder,
...localeThemebox,
...localeTable,
...localeApiTest,
},
dayjsLocale,

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