mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(projects): 1.0 beta
This commit is contained in:
@ -1,5 +0,0 @@
|
||||
import useCountDown from './use-count-down';
|
||||
import useSmsCode from './use-sms-code';
|
||||
import useImageVerify from './use-image-verify';
|
||||
|
||||
export { useCountDown, useSmsCode, useImageVerify };
|
@ -1,54 +0,0 @@
|
||||
import { computed, onScopeDispose, ref } from 'vue';
|
||||
import { useBoolean } from '../common';
|
||||
|
||||
/**
|
||||
* 倒计时
|
||||
* @param second - 倒计时的时间(s)
|
||||
*/
|
||||
export default function useCountDown(second: number) {
|
||||
if (second <= 0 && second % 1 !== 0) {
|
||||
throw new Error('倒计时的时间应该为一个正整数!');
|
||||
}
|
||||
const { bool: isComplete, setTrue, setFalse } = useBoolean(false);
|
||||
|
||||
const counts = ref(0);
|
||||
const isCounting = computed(() => Boolean(counts.value));
|
||||
|
||||
let intervalId: any;
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
* @param updateSecond - 更改初时传入的倒计时时间
|
||||
*/
|
||||
function start(updateSecond: number = second) {
|
||||
if (!counts.value) {
|
||||
setFalse();
|
||||
counts.value = updateSecond;
|
||||
intervalId = setInterval(() => {
|
||||
counts.value -= 1;
|
||||
if (counts.value <= 0) {
|
||||
clearInterval(intervalId);
|
||||
setTrue();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止计时
|
||||
*/
|
||||
function stop() {
|
||||
intervalId = clearInterval(intervalId);
|
||||
counts.value = 0;
|
||||
}
|
||||
|
||||
onScopeDispose(stop);
|
||||
|
||||
return {
|
||||
counts,
|
||||
isCounting,
|
||||
start,
|
||||
stop,
|
||||
isComplete
|
||||
};
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { PaginationProps, DataTableBaseColumn, DataTableSelectionColumn, DataTableExpandColumn } from 'naive-ui';
|
||||
import type { TableColumnGroup } from 'naive-ui/es/data-table/src/interface';
|
||||
import { useLoadingEmpty } from '../common';
|
||||
|
||||
/**
|
||||
* 接口请求函数
|
||||
*/
|
||||
type ApiFn<T = any, R = any> = (args: T) => Promise<Service.RequestResult<R>>;
|
||||
|
||||
/**
|
||||
* 接口请求函数的参数
|
||||
*/
|
||||
type GetApiFnParameters<T extends ApiFn, R = any> = T extends (args: infer P) => Promise<Service.RequestResult<R>>
|
||||
? P
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 接口请求函数的返回值
|
||||
*/
|
||||
type GetApiFnReturnType<T extends ApiFn, P = any> = T extends (args: P) => Promise<Service.RequestResult<infer R>>
|
||||
? R
|
||||
: never;
|
||||
|
||||
/**
|
||||
* 表格接口请求后转换后的数据
|
||||
*/
|
||||
type Transformer<TableData, Response> = (response: Response) => {
|
||||
data: TableData[];
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 列表接口参数更新
|
||||
*/
|
||||
type ApiParamsUpdater<P, R> = (params: P) => R;
|
||||
|
||||
/**
|
||||
* 分页参数
|
||||
*/
|
||||
type PagePropsOfPagination = Pick<PaginationProps, 'page' | 'pageSize'>;
|
||||
|
||||
/**
|
||||
* 自定义的列 key
|
||||
*/
|
||||
type CustomColumnKey<K = never> = K | 'action';
|
||||
|
||||
/**
|
||||
* 表格的列
|
||||
*/
|
||||
type HookTableColumn<T = Record<string, unknown>> =
|
||||
| (Omit<TableColumnGroup<T>, 'key'> & { key: CustomColumnKey<keyof T> })
|
||||
| (Omit<DataTableBaseColumn<T>, 'key'> & { key: CustomColumnKey<keyof T> })
|
||||
| DataTableSelectionColumn<T>
|
||||
| DataTableExpandColumn<T>;
|
||||
|
||||
/**
|
||||
* 表格配置
|
||||
*/
|
||||
type HookTableConfig<TableData, Fn extends ApiFn> = {
|
||||
/**
|
||||
* 列表接口参数
|
||||
*/
|
||||
apiParams: GetApiFnParameters<Fn>;
|
||||
/**
|
||||
* 列表接口返回数据转换
|
||||
*/
|
||||
transformer: Transformer<TableData, GetApiFnReturnType<Fn>>;
|
||||
/**
|
||||
* 列表列
|
||||
*/
|
||||
columns: () => HookTableColumn<TableData>[];
|
||||
/**
|
||||
* 列表接口参数更新
|
||||
* @description 用于更新分页参数, 如果列表接口的参数不包含同名分页参数属性 `page` 和 `pageSize`, 需要通过此函数更新
|
||||
* @default p => p
|
||||
*/
|
||||
apiParamsUpdater?: ApiParamsUpdater<GetApiFnParameters<Fn> & Partial<PagePropsOfPagination>, GetApiFnParameters<Fn>>;
|
||||
/**
|
||||
* 列表分页参数
|
||||
*/
|
||||
pagination?: PaginationProps;
|
||||
/**
|
||||
* 是否立即请求
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用表格 hook
|
||||
* @param apiFn 接口请求函数
|
||||
* @param config 表格配置
|
||||
*/
|
||||
export default function useHookTable<TableData, Fn extends ApiFn>(apiFn: Fn, config: HookTableConfig<TableData, Fn>) {
|
||||
const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty();
|
||||
|
||||
const { apiParams, transformer, apiParamsUpdater = p => p, immediate = true } = config;
|
||||
|
||||
const data: Ref<TableData[]> = ref([]);
|
||||
|
||||
function updateData(update: TableData[]) {
|
||||
data.value = update;
|
||||
}
|
||||
|
||||
const columns = ref(config.columns()) as Ref<HookTableColumn<TableData>[]>;
|
||||
|
||||
const requestParams = ref(apiParams) as Ref<HookTableConfig<TableData, Fn>['apiParams']>;
|
||||
|
||||
function updateRequestParamsByPagination(p: PagePropsOfPagination) {
|
||||
requestParams.value = apiParamsUpdater({ ...requestParams.value, ...p });
|
||||
}
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
onChange: (page: number) => {
|
||||
pagination.page = page;
|
||||
|
||||
updateRequestParamsByPagination({ page });
|
||||
getData();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
|
||||
updateRequestParamsByPagination({ pageSize });
|
||||
getData();
|
||||
},
|
||||
...config.pagination
|
||||
}) as PaginationProps;
|
||||
|
||||
function updatePagination(update: Partial<PaginationProps>) {
|
||||
Object.assign(pagination, update);
|
||||
|
||||
updateRequestParamsByPagination({ page: pagination.page, pageSize: pagination.pageSize });
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
startLoading();
|
||||
|
||||
const { data: apiData, error } = await apiFn(requestParams.value);
|
||||
|
||||
if (!error && data) {
|
||||
const { data: tableData, pageNum, pageSize, total } = transformer(apiData);
|
||||
|
||||
updateData(tableData);
|
||||
|
||||
setEmpty(tableData.length === 0);
|
||||
|
||||
updatePagination({ page: pageNum, pageSize, itemCount: total });
|
||||
}
|
||||
|
||||
endLoading();
|
||||
}
|
||||
|
||||
function reloadColumns() {
|
||||
columns.value = config.columns();
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
getData();
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
empty,
|
||||
pagination,
|
||||
getData,
|
||||
updatePagination,
|
||||
reloadColumns
|
||||
};
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 绘制图形验证码
|
||||
* @param width - 图形宽度
|
||||
* @param height - 图形高度
|
||||
*/
|
||||
export default function useImageVerify(width = 152, height = 40) {
|
||||
const domRef = ref<HTMLCanvasElement>();
|
||||
const imgCode = ref('');
|
||||
|
||||
function setImgCode(code: string) {
|
||||
imgCode.value = code;
|
||||
}
|
||||
|
||||
function getImgCode() {
|
||||
if (!domRef.value) return;
|
||||
imgCode.value = draw(domRef.value, width, height);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getImgCode();
|
||||
});
|
||||
|
||||
return {
|
||||
domRef,
|
||||
imgCode,
|
||||
setImgCode,
|
||||
getImgCode
|
||||
};
|
||||
}
|
||||
|
||||
function randomNum(min: number, max: number) {
|
||||
const num = Math.floor(Math.random() * (max - min) + min);
|
||||
return num;
|
||||
}
|
||||
|
||||
function randomColor(min: number, max: number) {
|
||||
const r = randomNum(min, max);
|
||||
const g = randomNum(min, max);
|
||||
const b = randomNum(min, max);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||
let imgCode = '';
|
||||
|
||||
const NUMBER_STRING = '0123456789';
|
||||
|
||||
const ctx = dom.getContext('2d');
|
||||
if (!ctx) return imgCode;
|
||||
|
||||
ctx.fillStyle = randomColor(180, 230);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||
imgCode += text;
|
||||
const fontSize = randomNum(18, 41);
|
||||
const deg = randomNum(-30, 30);
|
||||
ctx.font = `${fontSize}px Simhei`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = randomColor(80, 150);
|
||||
ctx.save();
|
||||
ctx.translate(30 * i + 23, 15);
|
||||
ctx.rotate((deg * Math.PI) / 180);
|
||||
ctx.fillText(text, -15 + 5, -15);
|
||||
ctx.restore();
|
||||
}
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.lineTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.strokeStyle = randomColor(180, 230);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < 41; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = randomColor(150, 200);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
return imgCode;
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { computed } from 'vue';
|
||||
import { REGEXP_PHONE } from '@/config';
|
||||
import { fetchSmsCode } from '@/service';
|
||||
import { useLoading } from '../common';
|
||||
import useCountDown from './use-count-down';
|
||||
|
||||
export default function useSmsCode() {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { counts, start, isCounting } = useCountDown(60);
|
||||
|
||||
const initLabel = '获取验证码';
|
||||
const countingLabel = (second: number) => `${second}秒后重新获取`;
|
||||
const label = computed(() => {
|
||||
let text = initLabel;
|
||||
if (loading.value) {
|
||||
text = '';
|
||||
}
|
||||
if (isCounting.value) {
|
||||
text = countingLabel(counts.value);
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
/** 判断手机号码格式是否正确 */
|
||||
function isPhoneValid(phone: string) {
|
||||
let valid = true;
|
||||
if (phone.trim() === '') {
|
||||
window.$message?.error('手机号码不能为空!');
|
||||
valid = false;
|
||||
} else if (!REGEXP_PHONE.test(phone)) {
|
||||
window.$message?.error('手机号码格式错误!');
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码
|
||||
* @param phone - 手机号
|
||||
*/
|
||||
async function getSmsCode(phone: string) {
|
||||
const valid = isPhoneValid(phone);
|
||||
if (!valid || loading.value) return;
|
||||
|
||||
startLoading();
|
||||
const { data } = await fetchSmsCode(phone);
|
||||
if (data) {
|
||||
window.$message?.success('验证码发送成功!');
|
||||
start();
|
||||
}
|
||||
endLoading();
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
start,
|
||||
isCounting,
|
||||
getSmsCode,
|
||||
loading
|
||||
};
|
||||
}
|
74
src/hooks/common/form.ts
Normal file
74
src/hooks/common/form.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { REG_USER_NAME, REG_PHONE, REG_PWD, REG_CODE_SIX, REG_EMAIL } from '@/constants/reg';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function useFormRules() {
|
||||
const constantRules = {
|
||||
userName: [
|
||||
createRequiredRule($t('form.userName.required')),
|
||||
{
|
||||
pattern: REG_USER_NAME,
|
||||
message: $t('form.userName.invalid'),
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
phone: [
|
||||
createRequiredRule($t('form.phone.required')),
|
||||
{
|
||||
pattern: REG_PHONE,
|
||||
message: $t('form.phone.invalid'),
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
pwd: [
|
||||
createRequiredRule($t('form.pwd.required')),
|
||||
{
|
||||
pattern: REG_PWD,
|
||||
message: $t('form.pwd.invalid'),
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
code: [
|
||||
createRequiredRule($t('form.code.required')),
|
||||
{
|
||||
pattern: REG_CODE_SIX,
|
||||
message: $t('form.code.invalid'),
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
email: [
|
||||
createRequiredRule($t('form.email.required')),
|
||||
{
|
||||
pattern: REG_EMAIL,
|
||||
message: $t('form.email.invalid'),
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
function createRequiredRule(message: string) {
|
||||
return {
|
||||
required: true,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
constantRules,
|
||||
createRequiredRule
|
||||
};
|
||||
}
|
||||
|
||||
export function useNaiveForm() {
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
|
||||
async function validate() {
|
||||
await formRef.value?.validate();
|
||||
}
|
||||
|
||||
return {
|
||||
formRef,
|
||||
validate
|
||||
};
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import useContext from './use-context';
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
import useLoadingEmpty from './use-loading-empty';
|
||||
import useReload from './use-reload';
|
||||
|
||||
export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload };
|
101
src/hooks/common/router.ts
Normal file
101
src/hooks/common/router.ts
Normal file
@ -0,0 +1,101 @@
|
||||
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
|
||||
* @description 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
|
||||
};
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* boolean组合式函数
|
||||
* @param initValue 初始值
|
||||
*/
|
||||
export default function useBoolean(initValue = false) {
|
||||
const bool = ref(initValue);
|
||||
|
||||
function setBool(value: boolean) {
|
||||
bool.value = value;
|
||||
}
|
||||
function setTrue() {
|
||||
setBool(true);
|
||||
}
|
||||
function setFalse() {
|
||||
setBool(false);
|
||||
}
|
||||
function toggle() {
|
||||
setBool(!bool.value);
|
||||
}
|
||||
|
||||
return {
|
||||
bool,
|
||||
setBool,
|
||||
setTrue,
|
||||
setFalse,
|
||||
toggle
|
||||
};
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/** 创建共享上下文状态 */
|
||||
export default function useContext<T>(contextName = 'context') {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
};
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import useBoolean from './use-boolean';
|
||||
|
||||
export default function useLoadingEmpty(initLoading = false, initEmpty = false) {
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initLoading);
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean(initEmpty);
|
||||
|
||||
return {
|
||||
loading,
|
||||
startLoading,
|
||||
endLoading,
|
||||
empty,
|
||||
setEmpty
|
||||
};
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import useBoolean from './use-boolean';
|
||||
|
||||
export default function useLoading(initValue = false) {
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
|
||||
|
||||
return {
|
||||
loading,
|
||||
startLoading,
|
||||
endLoading
|
||||
};
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { nextTick } from 'vue';
|
||||
import useBoolean from './use-boolean';
|
||||
|
||||
/** 重载 */
|
||||
export default function useReload() {
|
||||
// 重载的标志
|
||||
const { bool: reloadFlag, setTrue, setFalse } = useBoolean(true);
|
||||
|
||||
/**
|
||||
* 触发重载
|
||||
* @param duration - 延迟时间(ms)
|
||||
*/
|
||||
async function handleReload(duration = 0) {
|
||||
setFalse();
|
||||
await nextTick();
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
setTrue();
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reloadFlag,
|
||||
handleReload
|
||||
};
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './common';
|
||||
export * from './business';
|
Reference in New Issue
Block a user