feat(projects): 添加常用组件、composables函数

This commit is contained in:
Soybean
2021-12-12 17:28:39 +08:00
parent e755caabf2
commit 230a50a4cf
87 changed files with 5424 additions and 2071 deletions

View File

@ -1,4 +1,9 @@
import useAntv from './useAntv';
import useAntvTool from './useAntvTool';
import useCountDown from './useCountDown';
import useSmsCode from './useSmsCode';
import useImageVerify from './useImageVerify';
import useAgreement from './useAgreement';
import useVirtualList from './useVirtualList';
export { useCountDown, useSmsCode };
export { useAntv, useAntvTool, useCountDown, useSmsCode, useImageVerify, useAgreement, useVirtualList };

View File

@ -0,0 +1,20 @@
import { ref } from 'vue';
/** 使用勾选协议 */
export default function useAgreement(text = '请勾选 "我已经仔细阅读并接受《用户协议》《隐私权政策》"') {
const agreement = ref(true);
function isAgree() {
let agree = true;
if (!agreement.value) {
agree = false;
window.$message?.error(text);
}
return agree;
}
return {
agreement,
isAgree
};
}

View File

@ -0,0 +1,53 @@
import { ref, watch, onMounted } from 'vue';
import type { ComputedRef } from 'vue';
import type { Plot } from '@antv/g2plot';
import { useBoolean } from '@/hooks';
interface AntvFn<T, O> {
new (dom: HTMLElement, options: O): T;
}
export default function useAntv<GraphOption, GraphType extends Plot<GraphOption>>(
GraphFn: AntvFn<GraphType, GraphOption>,
graphOptions: ComputedRef<GraphOption>
) {
/** 图表dom容器 */
const domRef = ref<HTMLElement>();
/** 图表实例 */
const graph = ref<GraphType>();
/** 是否可以开始渲染图表 */
const { bool: canRender, setTrue: setCanRender } = useBoolean();
/** 是否是在onMouted第一次渲染图表 */
const { bool: isFirstRender, setTrue: setIsFirstRender, setFalse: setIsNotFirstRender } = useBoolean();
/** 渲染图表 */
function renderGraph(options: GraphOption) {
if (!domRef.value) return;
if (!graph.value) {
graph.value = new GraphFn(domRef.value, options);
graph.value.render();
} else {
graph.value.update(options);
}
}
onMounted(() => {
setCanRender();
setIsFirstRender();
renderGraph(graphOptions.value);
setIsNotFirstRender();
});
watch(graphOptions, newValue => {
if (!canRender.value || isFirstRender.value) return;
renderGraph(newValue);
});
return {
domRef,
graph
};
}

View File

@ -0,0 +1,31 @@
export default function useAntvTool() {
/**
* antv滑动调属性
*/
function getSlider(columns: number, length: number, sliderColor: string) {
return {
start: 1 - columns / length,
end: 1,
foregroundStyle: { fill: sliderColor }
};
}
function getFormatter(unit: string) {
const EMPTY = ' ';
function formatter(v: number | null) {
return v === null ? EMPTY : v + unit;
}
return formatter;
}
function formatLabelWithUnit(value: number | null, unit: string) {
const EMPTY = ' ';
return value === null ? EMPTY : value + unit;
}
return {
getSlider,
getFormatter,
formatLabelWithUnit
};
}

View File

@ -0,0 +1,85 @@
import { ref, onMounted } 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;
}

View File

@ -1,4 +1,5 @@
import { computed } from 'vue';
import { REGEXP_PHONE } from '@/config';
import useCountDown from './useCountDown';
export default function useSmsCode() {
@ -7,9 +8,35 @@ export default function useSmsCode() {
const countingLabel = (second: number) => `${second}秒后重新获取`;
const label = computed(() => (isCounting.value ? countingLabel(counts.value) : initLabel));
/** 判断手机号码格式是否正确 */
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) return;
// 该处调用验证码接口
window.$message?.success('验证码发送成功!');
start();
}
return {
label,
start,
isCounting
isCounting,
getSmsCode
};
}

View File

@ -0,0 +1,60 @@
import { ref, watch, computed, nextTick } from 'vue';
import type { Ref, ComputedRef } from 'vue';
interface VirtualListConfig {
/** 容器的高度 */
containerHeight: number;
/** 渲染的个数 */
renderNums: number;
/** 触发的高度距离 */
triggerHeight: number;
}
/**
* 虚拟列表
* @param list - 列表数据源
* @param config - 虚拟列表配置
*/
export default function useVirtualList<T extends { [key: string]: any }[]>(
list: Ref<T>,
config: VirtualListConfig = {
containerHeight: 200,
renderNums: 10,
triggerHeight: 24
}
) {
const { containerHeight, renderNums, triggerHeight } = config;
const renderIndex = ref(1);
function setRenderIndex(index: number) {
renderIndex.value = index;
}
function resetRenderIndex() {
setRenderIndex(1);
}
const dataSource = computed(() => {
const endIndex = renderIndex.value * renderNums;
return list.value.slice(0, endIndex);
}) as ComputedRef<T>;
function handleScroll(e: Event) {
const target = e.target as HTMLElement;
const needRender = target.scrollHeight - (target.scrollTop + containerHeight) < triggerHeight;
if (needRender) {
nextTick(() => {
setRenderIndex(renderIndex.value + 1);
});
}
}
watch(list, () => {
resetRenderIndex();
});
return {
containerHeight,
dataSource,
handleScroll
};
}

View File

@ -1,5 +1,6 @@
import useContext from './useContext';
import useBoolean from './useBoolean';
import useLoading from './useLoading';
import useLoadingEmpty from './useLoadingEmpty';
export { useContext, useBoolean, useLoading };
export { useContext, useBoolean, useLoading, useLoadingEmpty };

View File

@ -0,0 +1,14 @@
import useBoolean from './useBoolean';
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
};
}