Merge branch 'v2.0' into v2.0-example

This commit is contained in:
Soybean
2025-10-27 17:38:43 +08:00
94 changed files with 4172 additions and 2807 deletions

View File

@ -1,4 +1,4 @@
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode);
const domRef = ref<HTMLElement | null>(null);
const domRef = shallowRef<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
let chart: echarts.ECharts | null = null;
const chart = shallowRef<echarts.ECharts | null>(null);
const chartOptions: T = optionsFactory();
const {
@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
onDestroy
} = hooks;
/**
* whether can render chart
*
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
}
/** is chart rendered */
function isRendered() {
return Boolean(domRef.value && chart);
return Boolean(domRef.value && chart.value);
}
/**
@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
* @param callback callback function
*/
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
if (!isRendered()) return;
const updatedOpts = callback(chartOptions, optionsFactory);
Object.assign(chartOptions, updatedOpts);
await nextTick();
if (!isRendered()) return;
if (isRendered()) {
chart?.clear();
chart.value?.clear();
}
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
await onUpdated?.(chart!);
await onUpdated?.(chart.value!);
}
function setOptions(options: T) {
chart?.setOption(options);
chart.value?.setOption(options);
}
/** render chart */
async function render() {
if (!isRendered()) {
const chartTheme = darkMode.value ? 'dark' : 'light';
if (isRendered()) return;
await nextTick();
const chartTheme = darkMode.value ? 'dark' : 'light';
chart = echarts.init(domRef.value, chartTheme);
chart.value = echarts.init(domRef.value, chartTheme);
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
await onRender?.(chart);
}
await onRender?.(chart.value!);
}
/** resize chart */
function resize() {
chart?.resize();
chart.value?.resize();
}
/** destroy chart */
async function destroy() {
if (!chart) return;
if (!chart.value) return;
await onDestroy?.(chart);
chart?.dispose();
chart = null;
await onDestroy?.(chart.value);
chart.value?.dispose();
chart.value = null;
}
/** change chart theme */
async function changeTheme() {
await destroy();
await render();
await onUpdated?.(chart!);
await onUpdated?.(chart.value!);
}
/**
@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
initialSize.width = w;
initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart
if (isRendered()) {
resize();
return;
}
// render chart
await render();
if (chart) {
await onUpdated?.(chart);
if (chart.value) {
await onUpdated?.(chart.value);
}
}
scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
watch(
[width, height],
([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
},
{ flush: 'post' }
);
watch(darkMode, () => {
changeTheme();
@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
return {
domRef,
chart,
updateOptions,
setOptions
};

View File

@ -1,192 +1,55 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue';
import type { Ref } from 'vue';
import type { PaginationProps } from 'naive-ui';
import { useBoolean, useTable } from '@sa/hooks';
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
import type { FlatResponseData } from '@sa/axios';
import { jsonClone } from '@sa/utils';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
type TableData = NaiveUI.TableData;
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
type TableColumn<T> = NaiveUI.TableColumn<T>;
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
'pagination' | 'getColumnChecks' | 'getColumns'
> & {
/**
* get column visible
*
* @param column
*
* @default true
*
* @returns true if the column is visible, false otherwise
*/
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const { apiFn, apiParams, immediate, showTotal } = config;
const SELECTION_KEY = '__selection__';
const EXPAND_KEY = '__expand__';
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
// Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
const pageSize = size <= 0 ? 10 : size;
const recordsWithIndex = records.map((item, index) => {
return {
...item,
index: (current - 1) * pageSize + index + 1
};
});
return {
data: recordsWithIndex,
pageNum: current,
pageSize,
total
};
},
getColumnChecks: cols => {
const checks: NaiveUI.TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { pageNum, pageSize, total } = transformed;
updatePagination({
page: pageNum,
pageSize,
itemCount: total
});
},
immediate
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns
});
const pagination: PaginationProps = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
itemCount: 0,
pageSizes: [10, 15, 20, 25, 30],
onUpdatePage: async (page: number) => {
pagination.page = page;
updateSearchParams({
current: page,
size: pagination.pageSize!
});
getData();
},
onUpdatePageSize: async (pageSize: number) => {
pagination.pageSize = pageSize;
pagination.page = 1;
updateSearchParams({
current: pagination.page,
size: pageSize
});
getData();
},
...(showTotal
? {
prefix: page => $t('datatable.itemCount', { total: page.itemCount })
}
: {})
// calculate the total width of the table this is used for horizontal scrolling
const scrollX = computed(() => {
return result.columns.value.reduce((acc, column) => {
return acc + Number(column.width ?? column.minWidth ?? 120);
}, 0);
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
};
return p;
});
function updatePagination(update: Partial<PaginationProps>) {
Object.assign(pagination, update);
}
/**
* get data by page number
*
* @param pageNum the page number. default is 1
*/
async function getDataByPage(pageNum: number = 1) {
updatePagination({
page: pageNum
});
updateSearchParams({
current: pageNum,
size: pagination.pageSize!
});
await getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
result.reloadColumns();
}
);
});
@ -196,27 +59,126 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
});
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
...result,
scrollX
};
}
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
/**
* whether to show the total count of the table
*
* @default true
*/
showTotal?: boolean;
onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
};
export function useNaivePaginatedTable<ResponseData, ApiData>(
options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
) {
const scope = effectScope();
const appStore = useAppStore();
const isMobile = computed(() => appStore.isMobile);
const showTotal = computed(() => options.showTotal ?? true);
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 15, 20, 25, 30],
prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
onUpdatePage(page) {
pagination.page = page;
},
onUpdatePageSize(pageSize) {
pagination.pageSize = pageSize;
pagination.page = 1;
},
...options.paginationProps
}) as PaginationProps;
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: PaginationProps = {
...pagination,
pageSlot: isMobile.value ? 3 : 9,
prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
};
return p;
});
const paginationParams = computed(() => {
const { page, pageSize } = pagination;
return {
page,
pageSize
};
});
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options,
pagination: true,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns,
onFetched: data => {
pagination.itemCount = data.total;
}
});
async function getDataByPage(page: number = 1) {
if (page !== pagination.page) {
pagination.page = page;
return;
}
await result.getData();
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
result.reloadColumns();
}
);
watch(paginationParams, async newVal => {
await options.onPaginationParamsChange?.(newVal);
await result.getData();
});
});
onScopeDispose(() => {
scope.stop();
});
return {
...result,
getDataByPage,
pagination,
mobilePagination
};
}
export function useTableOperate<TableData>(
data: Ref<TableData[]>,
idKey: keyof TableData,
getData: () => Promise<void>
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<NaiveUI.TableOperateType>('add');
const operateType = shallowRef<NaiveUI.TableOperateType>('add');
function handleAdd() {
operateType.value = 'add';
@ -224,18 +186,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
}
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
const editingData = shallowRef<TableData | null>(null);
function handleEdit(id: T['id']) {
function handleEdit(id: TableData[keyof TableData]) {
operateType.value = 'edit';
const findItem = data.value.find(item => item.id === id) || null;
const findItem = data.value.find(item => item[idKey] === id) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = ref<string[]>([]);
const checkedRowKeys = shallowRef<string[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
@ -267,6 +229,82 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
};
}
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
export function defaultTransform<ApiData>(
response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
): PaginationData<ApiData> {
const { data, error } = response;
if (!error) {
const { records, current, size, total } = data;
return {
data: records,
pageNum: current,
pageSize: size,
total
};
}
return {
data: [],
pageNum: 1,
pageSize: 10,
total: 0
};
}
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
cols: Column[],
getColumnVisible?: (column: Column) => boolean
) {
const checks: TableColumnCheck[] = [];
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
checks.push({
key: column.key as string,
title: column.title!,
checked: true,
visible: getColumnVisible?.(column) ?? true
});
} else if (column.type === 'selection') {
checks.push({
key: SELECTION_KEY,
title: $t('common.check'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
} else if (column.type === 'expand') {
checks.push({
key: EXPAND_KEY,
title: $t('common.expandColumn'),
checked: true,
visible: getColumnVisible?.(column) ?? false
});
}
});
return checks;
}
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
const columnMap = new Map<string, Column>();
cols.forEach(column => {
if (isTableColumnHasKey(column)) {
columnMap.set(column.key as string, column);
} else if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column);
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
});
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
return filteredColumns;
}
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}