2
0

初始化项目

This commit is contained in:
caiyuchao
2024-11-14 11:06:38 +08:00
parent 988b9e6799
commit 4ffac789e1
320 changed files with 34244 additions and 0 deletions

230
src/hooks/common/echarts.ts Normal file
View File

@@ -0,0 +1,230 @@
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
import * as echarts from 'echarts/core';
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
import type {
BarSeriesOption,
GaugeSeriesOption,
LineSeriesOption,
PictorialBarSeriesOption,
PieSeriesOption,
RadarSeriesOption,
ScatterSeriesOption
} from 'echarts/charts';
import {
DatasetComponent,
GridComponent,
LegendComponent,
TitleComponent,
ToolboxComponent,
TooltipComponent,
TransformComponent
} from 'echarts/components';
import type {
DatasetComponentOption,
GridComponentOption,
LegendComponentOption,
TitleComponentOption,
ToolboxComponentOption,
TooltipComponentOption
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { useElementSize } from '@vueuse/core';
import { useThemeStore } from '@/store/modules/theme';
export type ECOption = echarts.ComposeOption<
| BarSeriesOption
| LineSeriesOption
| PieSeriesOption
| ScatterSeriesOption
| PictorialBarSeriesOption
| RadarSeriesOption
| GaugeSeriesOption
| TitleComponentOption
| LegendComponentOption
| TooltipComponentOption
| GridComponentOption
| ToolboxComponentOption
| DatasetComponentOption
>;
echarts.use([
TitleComponent,
LegendComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
ToolboxComponent,
BarChart,
LineChart,
PieChart,
ScatterChart,
PictorialBarChart,
RadarChart,
GaugeChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
interface ChartHooks {
onRender?: (chart: echarts.ECharts) => void | Promise<void>;
onUpdated?: (chart: echarts.ECharts) => void | Promise<void>;
onDestroy?: (chart: echarts.ECharts) => void | Promise<void>;
}
/**
* use echarts
*
* @param optionsFactory echarts options factory function
* @param darkMode dark mode
*/
export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: ChartHooks = {}) {
const scope = effectScope();
const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode);
const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
let chart: echarts.ECharts | null = null;
const chartOptions: T = optionsFactory();
const {
onRender = instance => {
const textColor = darkMode.value ? 'rgb(224, 224, 224)' : 'rgb(31, 31, 31)';
const maskColor = darkMode.value ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)';
instance.showLoading({
color: themeStore.themeColor,
textColor,
fontSize: 14,
maskColor
});
},
onUpdated = instance => {
instance.hideLoading();
},
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);
}
/**
* update chart options
*
* @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);
if (isRendered()) {
chart?.clear();
}
chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
await onUpdated?.(chart!);
}
/** render chart */
async function render() {
if (!isRendered()) {
const chartTheme = darkMode.value ? 'dark' : 'light';
await nextTick();
chart = echarts.init(domRef.value, chartTheme);
chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
await onRender?.(chart);
}
}
/** resize chart */
function resize() {
chart?.resize();
}
/** destroy chart */
async function destroy() {
if (!chart) return;
await onDestroy?.(chart);
chart?.dispose();
chart = null;
}
/** change chart theme */
async function changeTheme() {
await destroy();
await render();
await onUpdated?.(chart!);
}
/**
* render chart by size
*
* @param w width
* @param h height
*/
async function renderChartBySize(w: number, h: number) {
initialSize.width = w;
initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart
if (isRendered()) {
resize();
}
// render chart
await render();
}
scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
watch(darkMode, () => {
changeTheme();
});
});
onScopeDispose(() => {
destroy();
scope.stop();
});
return {
domRef,
updateOptions
};
}

97
src/hooks/common/form.ts Normal file
View File

@@ -0,0 +1,97 @@
import { ref, toValue } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import type { FormInstance } from 'ant-design-vue';
import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg';
import { $t } from '@/locales';
export function useFormRules() {
const patternRules = {
username: {
pattern: REG_USER_NAME,
message: $t('form.username.invalid'),
trigger: 'change'
},
phone: {
pattern: REG_PHONE,
message: $t('form.phone.invalid'),
trigger: 'change'
},
pwd: {
pattern: REG_PWD,
message: $t('form.pwd.invalid'),
trigger: 'change'
},
code: {
pattern: REG_CODE_SIX,
message: $t('form.code.invalid'),
trigger: 'change'
},
email: {
pattern: REG_EMAIL,
message: $t('form.email.invalid'),
trigger: 'change'
}
} satisfies Record<string, App.Global.FormRule>;
const formRules = {
username: [createRequiredRule($t('form.username.required')), patternRules.username],
phone: [createRequiredRule($t('form.phone.required')), patternRules.phone],
pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd],
code: [createRequiredRule($t('form.code.required')), patternRules.code],
email: [createRequiredRule($t('form.email.required')), patternRules.email]
} satisfies Record<string, App.Global.FormRule[]>;
/** the default required rule */
const defaultRequiredRule = createRequiredRule($t('form.required'));
function createRequiredRule(message: string) {
return {
required: true,
message
};
}
/** create a rule for confirming the password */
function createConfirmPwdRule(pwd: string | Ref<string> | ComputedRef<string>) {
const confirmPwdRule: App.Global.FormRule[] = [
{ required: true, message: $t('form.confirmPwd.required') },
{
validator: (rule, value) => {
if (value.trim() !== '' && value !== toValue(pwd)) {
return Promise.reject(rule.message);
}
return Promise.resolve();
},
message: $t('form.confirmPwd.invalid'),
trigger: 'change'
}
];
return confirmPwdRule;
}
return {
patternRules,
formRules,
defaultRequiredRule,
createRequiredRule,
createConfirmPwdRule
};
}
export function useAntdForm() {
const formRef = ref<FormInstance | null>(null);
async function validate() {
await formRef.value?.validate();
}
function resetFields() {
formRef.value?.resetFields();
}
return {
formRef,
validate,
resetFields
};
}

10
src/hooks/common/icon.ts Normal file
View File

@@ -0,0 +1,10 @@
import { useSvgIconRender } from '@sa/hooks';
import SvgIcon from '@/components/custom/svg-icon.vue';
export function useSvgIcon() {
const { SvgIconVNode } = useSvgIconRender(SvgIcon);
return {
SvgIconVNode
};
}

View File

@@ -0,0 +1,4 @@
export * from './echarts';
export * from './table';
export * from './form';
export * from './router';

103
src/hooks/common/router.ts Normal file
View File

@@ -0,0 +1,103 @@
import { useRouter } from 'vue-router';
import type { RouteLocationRaw } from 'vue-router';
import type { RouteKey } from '@elegant-router/types';
import { router as globalRouter } from '@/router';
/**
* Router push
*
* Jump to the specified route, it can replace function router.push
*
* @param inSetup Whether is in vue script setup
*/
export function useRouterPush(inSetup = true) {
const router = inSetup ? useRouter() : globalRouter;
const route = globalRouter.currentRoute;
const routerPush = router.push;
const routerBack = router.back;
interface RouterPushOptions {
query?: Record<string, string>;
params?: Record<string, string>;
}
async function routerPushByKey(key: RouteKey, options?: RouterPushOptions) {
const { query, params } = options || {};
const routeLocation: RouteLocationRaw = {
name: key
};
if (query) {
routeLocation.query = query;
}
if (params) {
routeLocation.params = params;
}
return routerPush(routeLocation);
}
async function toHome() {
return routerPushByKey('root');
}
/**
* Navigate to login page
*
* @param loginModule The login module
* @param redirectUrl The redirect url, if not specified, it will be the current route fullPath
*/
async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) {
const module = loginModule || 'pwd-login';
const options: RouterPushOptions = {
params: {
module
}
};
const redirect = redirectUrl || route.value.fullPath;
options.query = {
redirect
};
return routerPushByKey('login', options);
}
/**
* Toggle login module
*
* @param module
*/
async function toggleLoginModule(module: UnionKey.LoginModule) {
const query = route.value.query as Record<string, string>;
return routerPushByKey('login', { query, params: { module } });
}
/** Redirect from login */
async function redirectFromLogin() {
const redirect = route.value.query?.redirect as string;
if (redirect) {
routerPush(redirect);
} else {
toHome();
}
}
return {
route,
routerPush,
routerBack,
routerPushByKey,
toLogin,
toggleLoginModule,
redirectFromLogin
};
}

201
src/hooks/common/table.ts Normal file
View File

@@ -0,0 +1,201 @@
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
import type { Ref } from 'vue';
import type { TablePaginationConfig } from 'ant-design-vue';
import { useBoolean, useHookTable } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
type TableData<T = object> = AntDesign.TableData<T>;
type GetTableData<A extends AntDesign.TableApiFn> = AntDesign.GetTableData<A>;
type TableColumn<T> = AntDesign.TableColumn<T>;
export function useTable<A extends AntDesign.TableApiFn>(config: AntDesign.AntDesignTableConfig<A>) {
const scope = effectScope();
const appStore = useAppStore();
const { apiFn, apiParams, immediate, rowKey } = config;
const {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
} = useHookTable<A, GetTableData<A>, TableColumn<AntDesign.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
columns: config.columns,
transformer: res => {
const { rows = [], total = 0 } = res.data || {};
return {
rows: rows.map((row, index) => ({ ...row, id: rowKey ? row[rowKey] : index })),
total
};
},
getColumnChecks: cols => {
const checks: AntDesign.TableColumnCheck[] = [];
cols.forEach(column => {
if (column.key) {
checks.push({
key: column.key as string,
title: column.title as string,
checked: true
});
}
});
return checks;
},
getColumns: (cols, checks) => {
const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
cols.forEach(column => {
if (column.key) {
columnMap.set(column.key as string, column);
}
});
const filteredColumns = checks
.filter(item => item.checked)
.map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
return filteredColumns;
},
onFetched: async transformed => {
const { total } = transformed;
updatePagination({
total
});
},
immediate
});
const pagination: TablePaginationConfig = reactive({
showSizeChanger: true,
pageSizeOptions: [10, 15, 20, 25, 30],
total: 0,
simple: false,
// size: 'f',
current: 1,
pageSize: 10,
onChange: async (current: number, pageSize: number) => {
pagination.current = current;
updateSearchParams({
pageNum: current,
pageSize
});
getData();
}
});
// this is for mobile, if the system does not support mobile, you can use `pagination` directly
const mobilePagination = computed(() => {
const p: TablePaginationConfig = {
...pagination,
simple: appStore.isMobile
};
return p;
});
function updatePagination(update: Partial<TablePaginationConfig>) {
Object.assign(pagination, update);
}
scope.run(() => {
watch(
() => appStore.locale,
() => {
reloadColumns();
}
);
});
onScopeDispose(() => {
scope.stop();
});
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
pagination,
mobilePagination,
updatePagination,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}
export function useTableOperate<T extends TableData<{ [key: string]: any }>>(
data: Ref<T[]>,
options: {
getData: () => Promise<void>;
idKey?: string;
}
) {
const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
const operateType = ref<AntDesign.TableOperateType>('add');
const { getData, idKey = 'id' } = options;
/** the editing row data */
const editingData: Ref<T | null> = ref(null);
function handleAdd() {
operateType.value = 'add';
editingData.value = null;
openDrawer();
}
function handleEdit(id: any) {
operateType.value = 'edit';
editingData.value = data.value.find(item => item[idKey] === id) || null;
openDrawer();
}
/** the checked row keys of table */
const checkedRowKeys = ref<number[]>([]);
/** the hook after the batch delete operation is completed */
async function onBatchDeleted() {
$message?.success($t('common.deleteSuccess'));
checkedRowKeys.value = [];
await getData();
}
/** the hook after the delete operation is completed */
async function onDeleted() {
$message?.success($t('common.deleteSuccess'));
await getData();
}
return {
drawerVisible,
openDrawer,
closeDrawer,
operateType,
handleAdd,
editingData,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
};
}