From 292d760130543e1b8ba71a2a49198d3ce03f66a0 Mon Sep 17 00:00:00 2001 From: valarchie <343928303@qq.com> Date: Mon, 12 Jun 2023 22:05:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- README.md | 1 + index.html | 2 +- package.json | 2 + public/serverConfig.json | 2 +- src/components/ReImageVerify/index.ts | 7 + src/components/ReImageVerify/src/hooks.ts | 85 +++++++ src/components/ReImageVerify/src/index.vue | 46 ++++ src/components/ReQrcode/index.ts | 7 + src/components/ReQrcode/src/index.scss | 9 + src/components/ReQrcode/src/index.tsx | 261 +++++++++++++++++++++ src/components/ReTypeit/index.ts | 44 ++++ src/store/modules/types.ts | 1 + src/store/modules/user.ts | 10 +- src/views/login/components/phone.vue | 98 ++++++++ src/views/login/components/qrCode.vue | 22 ++ src/views/login/components/regist.vue | 185 +++++++++++++++ src/views/login/components/update.vue | 146 ++++++++++++ src/views/login/index.vue | 153 ++++++++++-- src/views/login/utils/enums.ts | 32 +++ src/views/login/utils/rule.ts | 103 +++++++- src/views/login/utils/verifyCode.ts | 50 ++++ 22 files changed, 1244 insertions(+), 25 deletions(-) create mode 100644 src/components/ReImageVerify/index.ts create mode 100644 src/components/ReImageVerify/src/hooks.ts create mode 100644 src/components/ReImageVerify/src/index.vue create mode 100644 src/components/ReQrcode/index.ts create mode 100644 src/components/ReQrcode/src/index.scss create mode 100644 src/components/ReQrcode/src/index.tsx create mode 100644 src/components/ReTypeit/index.ts create mode 100644 src/views/login/components/phone.vue create mode 100644 src/views/login/components/qrCode.vue create mode 100644 src/views/login/components/regist.vue create mode 100644 src/views/login/components/update.vue create mode 100644 src/views/login/utils/enums.ts create mode 100644 src/views/login/utils/verifyCode.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 1aa0ecb..9b434de 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,5 +27,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, - "iconify.excludes": ["el"] + "iconify.excludes": ["el"], + "cSpell.words": ["iconify", "Qrcode"] } diff --git a/README.md b/README.md index 1a39918..457ec99 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ ## 开发环境 node 版本应不小于 16 ,pnpm 版本应不小于 6 +版本请勿过新,有先选择 node=16, pnpm=6 如果您还没安装 pnpm,请执行下面命令进行安装(mac 用户遇到安装报错请在命令前加上 sudo) 如果是 windows 用户 用使用管理员 power shell 来执行 ``` diff --git a/index.html b/index.html index f7f6135..7aad001 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" /> - pure-admin-thin + Agileboot管理系统 + + diff --git a/src/components/ReQrcode/index.ts b/src/components/ReQrcode/index.ts new file mode 100644 index 0000000..48037a8 --- /dev/null +++ b/src/components/ReQrcode/index.ts @@ -0,0 +1,7 @@ +import reQrcode from "./src/index"; +import { withInstall } from "@pureadmin/utils"; + +/** 二维码组件 */ +export const ReQrcode = withInstall(reQrcode); + +export default ReQrcode; diff --git a/src/components/ReQrcode/src/index.scss b/src/components/ReQrcode/src/index.scss new file mode 100644 index 0000000..66ff634 --- /dev/null +++ b/src/components/ReQrcode/src/index.scss @@ -0,0 +1,9 @@ +.qrcode { + &--disabled { + background: rgb(255 255 255 / 95%); + + & > div { + transform: translate(-50%, -50%); + } + } +} diff --git a/src/components/ReQrcode/src/index.tsx b/src/components/ReQrcode/src/index.tsx new file mode 100644 index 0000000..7374402 --- /dev/null +++ b/src/components/ReQrcode/src/index.tsx @@ -0,0 +1,261 @@ +import { + ref, + unref, + watch, + nextTick, + computed, + PropType, + defineComponent +} from "vue"; +import "./index.scss"; +import propTypes from "@/utils/propTypes"; +import { isString, cloneDeep } from "@pureadmin/utils"; +import QRCode, { QRCodeRenderersOptions } from "qrcode"; +import RefreshRight from "@iconify-icons/ep/refresh-right"; + +interface QrcodeLogo { + src?: string; + logoSize?: number; + bgColor?: string; + borderSize?: number; + crossOrigin?: string; + borderRadius?: number; + logoRadius?: number; +} + +const props = { + // img 或者 canvas,img不支持logo嵌套 + tag: propTypes.string + .validate((v: string) => ["canvas", "img"].includes(v)) + .def("canvas"), + // 二维码内容 + text: { + type: [String, Array] as PropType, + default: null + }, + // qrcode.js配置项 + options: { + type: Object as PropType, + default: (): QRCodeRenderersOptions => ({}) + }, + // 宽度 + width: propTypes.number.def(200), + // logo + logo: { + type: [String, Object] as PropType | string>, + default: (): QrcodeLogo | string => "" + }, + // 是否过期 + disabled: propTypes.bool.def(false), + // 过期提示内容 + disabledText: propTypes.string.def("") +}; + +export default defineComponent({ + name: "ReQrcode", + props, + emits: ["done", "click", "disabled-click"], + setup(props, { emit }) { + const { toCanvas, toDataURL } = QRCode; + const loading = ref(true); + const wrapRef = ref>(null); + const renderText = computed(() => String(props.text)); + const wrapStyle = computed(() => { + return { + width: props.width + "px", + height: props.width + "px" + }; + }); + const initQrcode = async () => { + await nextTick(); + const options = cloneDeep(props.options || {}); + if (props.tag === "canvas") { + // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率 + options.errorCorrectionLevel = + options.errorCorrectionLevel || + getErrorCorrectionLevel(unref(renderText)); + const _width: number = await getOriginWidth(unref(renderText), options); + options.scale = + props.width === 0 ? undefined : (props.width / _width) * 4; + const canvasRef: any = await toCanvas( + unref(wrapRef) as HTMLCanvasElement, + unref(renderText), + options + ); + if (props.logo) { + const url = await createLogoCode(canvasRef); + emit("done", url); + loading.value = false; + } else { + emit("done", canvasRef.toDataURL()); + loading.value = false; + } + } else { + const url = await toDataURL(renderText.value, { + errorCorrectionLevel: "H", + width: props.width, + ...options + }); + (unref(wrapRef) as any).src = url; + emit("done", url); + loading.value = false; + } + }; + watch( + () => renderText.value, + val => { + if (!val) return; + initQrcode(); + }, + { + deep: true, + immediate: true + } + ); + const createLogoCode = (canvasRef: HTMLCanvasElement) => { + const canvasWidth = canvasRef.width; + const logoOptions: QrcodeLogo = Object.assign( + { + logoSize: 0.15, + bgColor: "#ffffff", + borderSize: 0.05, + crossOrigin: "anonymous", + borderRadius: 8, + logoRadius: 0 + }, + isString(props.logo) ? {} : props.logo + ); + const { + logoSize = 0.15, + bgColor = "#ffffff", + borderSize = 0.05, + crossOrigin = "anonymous", + borderRadius = 8, + logoRadius = 0 + } = logoOptions; + const logoSrc = isString(props.logo) ? props.logo : props.logo.src; + const logoWidth = canvasWidth * logoSize; + const logoXY = (canvasWidth * (1 - logoSize)) / 2; + const logoBgWidth = canvasWidth * (logoSize + borderSize); + const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2; + const ctx = canvasRef.getContext("2d"); + if (!ctx) return; + // logo 底色 + canvasRoundRect(ctx)( + logoBgXY, + logoBgXY, + logoBgWidth, + logoBgWidth, + borderRadius + ); + ctx.fillStyle = bgColor; + ctx.fill(); + // logo + const image = new Image(); + if (crossOrigin || logoRadius) { + image.setAttribute("crossOrigin", crossOrigin); + } + (image as any).src = logoSrc; + // 使用image绘制可以避免某些跨域情况 + const drawLogoWithImage = (image: HTMLImageElement) => { + ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth); + }; + // 使用canvas绘制以获得更多的功能 + const drawLogoWithCanvas = (image: HTMLImageElement) => { + const canvasImage = document.createElement("canvas"); + canvasImage.width = logoXY + logoWidth; + canvasImage.height = logoXY + logoWidth; + const imageCanvas = canvasImage.getContext("2d"); + if (!imageCanvas || !ctx) return; + imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth); + canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius); + if (!ctx) return; + const fillStyle = ctx.createPattern(canvasImage, "no-repeat"); + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + }; + // 将 logo绘制到 canvas上 + return new Promise((resolve: any) => { + image.onload = () => { + logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image); + resolve(canvasRef.toDataURL()); + }; + }); + }; + // 得到原QrCode的大小,以便缩放得到正确的QrCode大小 + const getOriginWidth = async ( + content: string, + options: QRCodeRenderersOptions + ) => { + const _canvas = document.createElement("canvas"); + await toCanvas(_canvas, content, options); + return _canvas.width; + }; + // 对于内容少的QrCode,增大容错率 + const getErrorCorrectionLevel = (content: string) => { + if (content.length > 36) { + return "M"; + } else if (content.length > 16) { + return "Q"; + } else { + return "H"; + } + }; + // 用于绘制圆角 + const canvasRoundRect = (ctx: CanvasRenderingContext2D) => { + return (x: number, y: number, w: number, h: number, r: number) => { + const minSize = Math.min(w, h); + if (r > minSize / 2) { + r = minSize / 2; + } + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + return ctx; + }; + }; + const clickCode = () => { + emit("click"); + }; + const disabledClick = () => { + emit("disabled-click"); + }; + return () => ( + <> +
+ {props.tag === "canvas" ? ( + + ) : ( + + )} + {props.disabled && ( +
+
+ +
{props.disabledText}
+
+
+ )} +
+ + ); + } +}); diff --git a/src/components/ReTypeit/index.ts b/src/components/ReTypeit/index.ts new file mode 100644 index 0000000..4c34bae --- /dev/null +++ b/src/components/ReTypeit/index.ts @@ -0,0 +1,44 @@ +import { h, defineComponent } from "vue"; +import TypeIt from "typeit"; + +// 打字机效果组件(只是简单的封装下,更多配置项参考 https://www.typeitjs.com/docs/vanilla/usage#options) +export default defineComponent({ + name: "TypeIt", + props: { + /** 打字速度,以每一步之间的毫秒数为单位,默认`200` */ + speed: { + type: Number, + default: 200 + }, + values: { + type: Array, + defalut: [] + }, + className: { + type: String, + default: "type-it" + }, + cursor: { + type: Boolean, + default: true + } + }, + render() { + return h( + "span", + { + class: this.className + }, + { + default: () => [] + } + ); + }, + mounted() { + new TypeIt(`.${this.className}`, { + strings: this.values, + speed: this.speed, + cursor: this.cursor + }).go(); + } +}); diff --git a/src/store/modules/types.ts b/src/store/modules/types.ts index be63f00..c681b81 100644 --- a/src/store/modules/types.ts +++ b/src/store/modules/types.ts @@ -39,4 +39,5 @@ export type setType = { export type userType = { username?: string; roles?: Array; + currentPage?: number; }; diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 5d6b337..4ee3d8d 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -10,13 +10,15 @@ import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { type DataInfo, setToken, removeToken, sessionKey } from "@/utils/auth"; export const useUserStore = defineStore({ - id: "pure-user", + id: "ag-user", state: (): userType => ({ // 用户名 username: storageSession().getItem>(sessionKey)?.username ?? "", // 页面级别权限 - roles: storageSession().getItem>(sessionKey)?.roles ?? [] + roles: storageSession().getItem>(sessionKey)?.roles ?? [], + // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码) + currentPage: 0 }), actions: { /** 存储用户名 */ @@ -27,6 +29,10 @@ export const useUserStore = defineStore({ SET_ROLES(roles: Array) { this.roles = roles; }, + /** 存储登录页面显示哪个组件 */ + SET_CURRENTPAGE(value: number) { + this.currentPage = value; + }, /** 登入 */ async loginByUsername(data) { return new Promise((resolve, reject) => { diff --git a/src/views/login/components/phone.vue b/src/views/login/components/phone.vue new file mode 100644 index 0000000..73944be --- /dev/null +++ b/src/views/login/components/phone.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/views/login/components/qrCode.vue b/src/views/login/components/qrCode.vue new file mode 100644 index 0000000..f37a1d6 --- /dev/null +++ b/src/views/login/components/qrCode.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/views/login/components/regist.vue b/src/views/login/components/regist.vue new file mode 100644 index 0000000..2edb391 --- /dev/null +++ b/src/views/login/components/regist.vue @@ -0,0 +1,185 @@ + + + diff --git a/src/views/login/components/update.vue b/src/views/login/components/update.vue new file mode 100644 index 0000000..e9477ff --- /dev/null +++ b/src/views/login/components/update.vue @@ -0,0 +1,146 @@ + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue index a3f4efa..1ad6f8f 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -1,16 +1,30 @@