diff --git a/eslint.config.js b/eslint.config.js index c2329f66..3762778a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,6 +9,13 @@ export default defineConfig( { ignores: ['index', 'App', '[id]'] } + ], + 'vue/component-name-in-template-casing': [ + 'warn', + 'PascalCase', + { + ignores: ['/^icon-/'] + } ] } } diff --git a/package.json b/package.json index 1231fbda..9a110432 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "nprogress": "0.2.0", "pinia": "2.1.7", "vue": "3.4.15", + "vue-draggable-plus": "^0.3.5", "vue-i18n": "9.9.0", "vue-router": "4.2.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0618fb10..3c8e7130 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: vue: specifier: 3.4.15 version: 3.4.15(typescript@5.3.3) + vue-draggable-plus: + specifier: ^0.3.5 + version: 0.3.5(@types/sortablejs@1.15.7) vue-i18n: specifier: 9.9.0 version: 9.9.0(vue@3.4.15) @@ -1869,6 +1872,10 @@ packages: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} dev: true + /@types/sortablejs@1.15.7: + resolution: {integrity: sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==} + dev: false + /@types/svgo@2.6.4: resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} dependencies: @@ -8625,6 +8632,18 @@ packages: dependencies: vue: 3.4.15(typescript@5.3.3) + /vue-draggable-plus@0.3.5(@types/sortablejs@1.15.7): + resolution: {integrity: sha512-HqIxV4Wr4U5LRPLRi2oV+EJ4g6ibyRKhuaiH4ZQo+LxK4zrk2XcBk9UyXC88OXp4SAq0XYH4Wco/T3LX5kJ79A==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + '@types/sortablejs': 1.15.7 + dev: false + /vue-eslint-parser@9.4.2(eslint@8.56.0): resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} engines: {node: ^14.17.0 || >=16.0.0} diff --git a/src/components/advanced/table-column-setting.vue b/src/components/advanced/table-column-setting.vue new file mode 100644 index 00000000..15730e91 --- /dev/null +++ b/src/components/advanced/table-column-setting.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/constants/business.ts b/src/constants/business.ts new file mode 100644 index 00000000..e14aa1f5 --- /dev/null +++ b/src/constants/business.ts @@ -0,0 +1,8 @@ +import { transformRecordToOption } from '@/utils/common'; + +export const roleStatusRecord: Record = { + '1': 'page.manage.role.status.enable', + '2': 'page.manage.role.status.disable' +}; + +export const roleStatusOptions = transformRecordToOption(roleStatusRecord); diff --git a/src/hooks/common/form.ts b/src/hooks/common/form.ts index 27432554..acc93560 100644 --- a/src/hooks/common/form.ts +++ b/src/hooks/common/form.ts @@ -47,7 +47,10 @@ export function useFormRules() { ] } satisfies Record; - function createRequiredRule(message: string) { + /** the default required rule */ + const defaultRequiredRule = createRequiredRule($t('form.required')); + + function createRequiredRule(message: string): App.Global.FormRule { return { required: true, message @@ -56,6 +59,7 @@ export function useFormRules() { return { constantRules, + defaultRequiredRule, createRequiredRule }; } diff --git a/src/hooks/common/table.ts b/src/hooks/common/table.ts new file mode 100644 index 00000000..b41dcf68 --- /dev/null +++ b/src/hooks/common/table.ts @@ -0,0 +1,223 @@ +import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue'; +import type { Ref } from 'vue'; +import type { DataTableBaseColumn, DataTableExpandColumn, DataTableSelectionColumn, PaginationProps } from 'naive-ui'; +import type { TableColumnGroup } from 'naive-ui/es/data-table/src/interface'; +import { useBoolean, useLoading } from '@sa/hooks'; +import { useAppStore } from '@/store/modules/app'; + +type BaseData = Record; + +type ApiFn = (args: any) => Promise; + +export type TableColumn = + | (Omit, 'key'> & { key: keyof T | CustomColumnKey }) + | (Omit, 'key'> & { key: keyof T | CustomColumnKey }) + | DataTableSelectionColumn + | DataTableExpandColumn; + +export type TransformedData = { + data: TableData[]; + pageNum: number; + pageSize: number; + total: number; +}; + +/** transform api response to table data */ +type Transformer> = ( + response: Response +) => TransformedData; + +/** table config */ +export type TableConfig = { + /** api function to get table data */ + apiFn: Fn; + /** api params */ + apiParams: Parameters[0]; + /** transform api response to table data */ + transformer: Transformer>>; + /** pagination */ + pagination?: PaginationProps; + /** + * callback when pagination changed + * + * @param pagination + */ + onPaginationChanged?: (pagination: PaginationProps) => void | Promise; + /** + * whether to get data immediately + * + * @default true + */ + immediate?: boolean; + /** columns factory */ + columns: () => TableColumn[]; +}; + +/** filter columns */ +export type FilteredColumn = { + key: string; + title: string; + checked: boolean; +}; + +export function useTable( + config: TableConfig +) { + const scope = effectScope(); + const appStore = useAppStore(); + + const { loading, startLoading, endLoading } = useLoading(); + const { bool: empty, setBool: setEmpty } = useBoolean(); + + const { apiFn, apiParams, transformer, onPaginationChanged, immediate = true } = config; + + const searchParams: NonNullable[0]> = reactive(apiParams || {}); + + const { columns, filteredColumns, reloadColumns } = useTableColumn(config.columns); + + const data: Ref = ref([]); + + const pagination = reactive({ + page: 1, + pageSize: 10, + showSizePicker: true, + pageSizes: [10, 15, 20, 25, 30], + onChange: async (page: number) => { + pagination.page = page; + + await onPaginationChanged?.(pagination); + }, + onUpdatePageSize: async (pageSize: number) => { + pagination.pageSize = pageSize; + pagination.page = 1; + + await onPaginationChanged?.(pagination); + }, + ...config.pagination + }) as PaginationProps; + + function updatePagination(update: Partial) { + Object.assign(pagination, update); + } + + async function getData() { + startLoading(); + + const response = await apiFn(searchParams); + + const { data: tableData, pageNum, pageSize, total } = transformer(response as Awaited>); + + data.value = tableData; + + setEmpty(tableData.length === 0); + updatePagination({ page: pageNum, pageSize, itemCount: total }); + endLoading(); + } + + /** + * update search params + * + * @param params + */ + function updateSearchParams(params: Partial[0]>) { + Object.assign(searchParams, params); + } + + /** reset search params */ + function resetSearchParams() { + Object.keys(searchParams).forEach(key => { + searchParams[key as keyof typeof searchParams] = undefined; + }); + } + + if (immediate) { + getData(); + } + + scope.run(() => { + watch( + () => appStore.locale, + () => { + reloadColumns(); + } + ); + }); + + onScopeDispose(() => { + scope.stop(); + }); + + return { + loading, + empty, + data, + columns, + filteredColumns, + reloadColumns, + pagination, + updatePagination, + getData, + searchParams, + resetSearchParams, + updateSearchParams + }; +} + +function useTableColumn( + factory: () => TableColumn[] +) { + const SELECTION_KEY = '__selection__'; + + const allColumns = ref(factory()) as Ref[]>; + + const filteredColumns: Ref = ref(getFilteredColumns(factory())); + + const columns = computed(() => getColumns()); + + function reloadColumns() { + allColumns.value = factory(); + } + + function getFilteredColumns(aColumns: TableColumn[]) { + const cols: FilteredColumn[] = []; + + aColumns.forEach(column => { + if (column.type === undefined) { + cols.push({ + key: column.key as string, + title: column.title as string, + checked: true + }); + } + + if (column.type === 'selection') { + cols.push({ + key: SELECTION_KEY, + title: '勾选', + checked: true + }); + } + }); + + return cols; + } + + function getColumns() { + const cols = filteredColumns.value + .filter(column => column.checked) + .map(column => { + if (column.key === SELECTION_KEY) { + return allColumns.value.find(col => col.type === 'selection'); + } + return allColumns.value.find(col => (col as DataTableBaseColumn).key === column.key); + }); + + return cols as TableColumn[]; + } + + return { + columns, + reloadColumns, + filteredColumns + }; +} diff --git a/src/layouts/modules/theme-drawer/index.vue b/src/layouts/modules/theme-drawer/index.vue index 63fa5214..aa937d5a 100644 --- a/src/layouts/modules/theme-drawer/index.vue +++ b/src/layouts/modules/theme-drawer/index.vue @@ -15,7 +15,7 @@ const appStore = useAppStore();