From 0adc99c7f4a38ec410ea5706fe7dae731e317bf4 Mon Sep 17 00:00:00 2001 From: lijianxiong <1518062161@qq.com> Date: Wed, 11 Dec 2024 10:19:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E9=98=85=E8=AF=BB=E5=99=A8=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++ src/editor/index.ts | 7 +++ .../qrcode/ibiz-qrcode/ibiz-qrcode.scss | 11 ++++ src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.tsx | 63 +++++++++++++++++++ src/editor/qrcode/index.ts | 3 + src/editor/qrcode/qrcode-editor.controller.ts | 13 ++++ src/editor/qrcode/qrcode-editor.provider.ts | 30 +++++++++ 7 files changed, 131 insertions(+) create mode 100644 src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.scss create mode 100644 src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.tsx create mode 100644 src/editor/qrcode/index.ts create mode 100644 src/editor/qrcode/qrcode-editor.controller.ts create mode 100644 src/editor/qrcode/qrcode-editor.provider.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2172c2b15a7..8b3e12698a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增二维码阅读器编辑器 + ## [0.0.38] - 2024-12-06 ### Added diff --git a/src/editor/index.ts b/src/editor/index.ts index 54f72fb3111..6fe9e64a8e6 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -39,6 +39,7 @@ import { ColorPickerEditorProvider, IBizColorPicker } from './color-picker'; import { MarkDownEditorProvider } from './markdown'; import { HtmlEditorProvider } from './html'; import { IBizDropdownList } from './dropdown-list/ibiz-dropdown-list/ibiz-dropdown-list'; +import { IBizQrcode, QrcodeEditorProvider } from './qrcode'; export const IBizEditor = { install: (v: App): void => { @@ -69,6 +70,7 @@ export const IBizEditor = { v.component(IBizColorPicker.name, IBizColorPicker); v.component(IBizPickerSelectView.name, IBizPickerSelectView); v.component(IBizEditorCarousel.name, IBizEditorCarousel); + v.component(IBizQrcode.name, IBizQrcode); v.component( 'IBizMarkDown', @@ -280,6 +282,11 @@ export const IBizEditor = { 'AUTH_PASSWORD_PASSWORD', () => textBoxEditorProvider, ); + // 二维码阅读器 + registerEditorProvider( + 'MOB2DBARCODEREADER', + () => new QrcodeEditorProvider(), + ); }, }; diff --git a/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.scss b/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.scss new file mode 100644 index 00000000000..cfc1a22cac4 --- /dev/null +++ b/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.scss @@ -0,0 +1,11 @@ +@include b(qrcode) { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + + @include e('no-img') { + color: getCssVar(color, disabled, text); + } +} \ No newline at end of file diff --git a/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.tsx b/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.tsx new file mode 100644 index 00000000000..1fe2d036ccd --- /dev/null +++ b/src/editor/qrcode/ibiz-qrcode/ibiz-qrcode.tsx @@ -0,0 +1,63 @@ +import { defineComponent, ref, watch } from 'vue'; +import { + getEditorEmits, + getRawProps, + useNamespace, +} from '@ibiz-template/vue3-util'; +import { isNil } from 'ramda'; +import { QrcodeEditorController } from '../qrcode-editor.controller'; +import './ibiz-qrcode.scss'; + +export const IBizQrcode = defineComponent({ + name: 'IBizQrcode', + props: getRawProps(), + emits: getEditorEmits(), + setup(props, { attrs }) { + const ns = useNamespace('qrcode'); + + // 转换的二维码图片路径 + const dataUrl = ref(''); + + watch( + () => props.value, + async (newVal, oldVal) => { + let text = ''; + if (newVal !== oldVal) { + if (isNil(newVal)) { + text = ''; + } else { + text = `${newVal}`; + } + } + if (text) { + const qrCode = ibiz.qrcodeUtil.createQrcode(text, { + margin: 8, + ...attrs, + }); + const element = await qrCode._getElement(); + dataUrl.value = element.toDataURL(); + } + }, + { + immediate: true, + }, + ); + return { + ns, + dataUrl, + }; + }, + render() { + let content = ( + + ); + if (this.dataUrl) { + content = ; + } + return
{content}
; + }, +}); diff --git a/src/editor/qrcode/index.ts b/src/editor/qrcode/index.ts new file mode 100644 index 00000000000..81d8a03054d --- /dev/null +++ b/src/editor/qrcode/index.ts @@ -0,0 +1,3 @@ +export { IBizQrcode } from './ibiz-qrcode/ibiz-qrcode'; +export * from './qrcode-editor.controller'; +export * from './qrcode-editor.provider'; diff --git a/src/editor/qrcode/qrcode-editor.controller.ts b/src/editor/qrcode/qrcode-editor.controller.ts new file mode 100644 index 00000000000..2d471deec5e --- /dev/null +++ b/src/editor/qrcode/qrcode-editor.controller.ts @@ -0,0 +1,13 @@ +import { EditorController } from '@ibiz-template/runtime'; +import { IBarCode2DReader } from '@ibiz/model-core'; + +/** + * 二维码阅读器编辑器控制器 + * + * @author ljx + * @date 2024-12-10 10:09:03 + * @export + * @class QrcodeEditorController + * @extends {EditorController} + */ +export class QrcodeEditorController extends EditorController {} diff --git a/src/editor/qrcode/qrcode-editor.provider.ts b/src/editor/qrcode/qrcode-editor.provider.ts new file mode 100644 index 00000000000..372511d85d5 --- /dev/null +++ b/src/editor/qrcode/qrcode-editor.provider.ts @@ -0,0 +1,30 @@ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { IBarCode2DReader } from '@ibiz/model-core'; +import { QrcodeEditorController } from './qrcode-editor.controller'; + +/** + * 二维码阅读器编辑器适配器 + * + * @author ljx + * @date 2024-12-10 10:09:03 + * @export + * @class QrcodeEditorProvider + * @implements {EditorProvider} + */ +export class QrcodeEditorProvider implements IEditorProvider { + formEditor: string = 'IBizQrcode'; + + gridEditor: string = 'IBizQrcode'; + + async createController( + editorModel: IBarCode2DReader, + parentController: IEditorContainerController, + ): Promise { + const c = new QrcodeEditorController(editorModel, parentController); + await c.init(); + return c; + } +} -- Gitee From af63b21cfc8b1d8c1a9ef9667059aba32e5786ca Mon Sep 17 00:00:00 2001 From: lijianxiong <1518062161@qq.com> Date: Wed, 11 Dec 2024 10:20:56 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + package.json | 4 +- src/locale/en/index.ts | 10 ++ src/locale/zh-CN/index.ts | 10 ++ src/mob-app/main.ts | 2 + src/util/index.ts | 1 + src/util/qrcode-util/qrcode-util.ts | 27 +++++ src/util/scan-qrcode/scan-qrcode.scss | 99 ++++++++++++++++ src/util/scan-qrcode/scan-qrcode.tsx | 160 ++++++++++++++++++++++++++ 9 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 src/util/qrcode-util/qrcode-util.ts create mode 100644 src/util/scan-qrcode/scan-qrcode.scss create mode 100644 src/util/scan-qrcode/scan-qrcode.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b3e12698a6..bdd6eb38941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Added - 新增二维码阅读器编辑器 +- 新增二维码工具类 ## [0.0.38] - 2024-12-06 diff --git a/package.json b/package.json index 25d9075790f..b9c1e983499 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "vant": "^4.7.2", "vue": "^3.3.8", "vue-i18n": "^9.6.5", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "qr-code-styling": "^1.8.3", + "vue-qrcode-reader": "5.5.11" }, "devDependencies": { "@commitlint/cli": "^18.4.1", diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 1bef4d54b36..56b67ceab52 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -278,6 +278,16 @@ export default { unrealized: 'Unrealized', cacheWarningPrompt: 'There is only one item in the cache stack and it cannot be navigated back', + scanQrcode: { + notAllowedError: 'You need to grant camera access permission', + notFoundError: 'No camera on this device', + notSupportedError: 'Secure context required (HTTPS, localhost)', + notReadableError: 'Is the camera already in use?', + overconstrainedError: 'Installed cameras are not suitable', + streamApiNotSupportedError: 'Stream API is not supported in this browser', + insecureContextError: + 'Camera access is only permitted in secure context. use HTTPS or localhost rather than HTTP.', + }, }, // 视图 view: { diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 4ed140d6228..33f28642e9f 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -266,6 +266,16 @@ export default { notAchieved: '没有实现', unrealized: '未实现', cacheWarningPrompt: '堆栈只有一个缓存不能后退了', + scanQrcode: { + notAllowedError: '您需要授予相机访问权限', + notFoundError: '此设备上没有摄像头', + notSupportedError: '需要安全上下文(HTTPS,localhost)', + notReadableError: '相机是否已在使用中?', + overconstrainedError: '安装的摄像头不合适', + streamApiNotSupportedError: '此浏览器不支持流API', + insecureContextError: + '只有在安全的情况下才允许访问摄像头。使用HTTPS或localhost而不是HTTP。', + }, }, // 视图 view: { diff --git a/src/mob-app/main.ts b/src/mob-app/main.ts index 252bc01fff8..ce6cf3fe7be 100644 --- a/src/mob-app/main.ts +++ b/src/mob-app/main.ts @@ -28,6 +28,7 @@ import { OpenViewUtil, OverlayController, FullscreenUtil, + QrcodeUtil, } from '../util'; export async function runApp(plugins?: Plugin[]): Promise { @@ -91,6 +92,7 @@ export async function runApp(plugins?: Plugin[]): Promise { ibiz.util.text.format = (value, code): string => { return app.config.globalProperties.$textFormat(value, code); }; + ibiz.qrcodeUtil = new QrcodeUtil(); await ibiz.i18n.init(); diff --git a/src/util/index.ts b/src/util/index.ts index 6e8090c21e0..02bc696edfe 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -11,3 +11,4 @@ export { usePagination } from './pagination/use-pagination'; export { FullscreenUtil } from './fullscreen/fullscreen-util'; export * from './store'; export { usePopstateListener } from './use-popstate-util/use-popstate-util'; +export { QrcodeUtil } from './qrcode-util/qrcode-util'; diff --git a/src/util/qrcode-util/qrcode-util.ts b/src/util/qrcode-util/qrcode-util.ts new file mode 100644 index 00000000000..11afc55d890 --- /dev/null +++ b/src/util/qrcode-util/qrcode-util.ts @@ -0,0 +1,27 @@ +import { IQrcodeUtil } from '@ibiz-template/runtime'; +import QRCodeStyling from 'qr-code-styling'; +import { createScanQrcode } from '../scan-qrcode/scan-qrcode'; + +/** + * 二维码工具类 + * + * @description 此实现类挂载在 ibiz.qrcodeUtil + * @author ljx + * @date 2024-12-10 10:12:50 + * @export + * @class QrcodeUtil + * @implements {IQrcodeUtil} + */ +export class QrcodeUtil implements IQrcodeUtil { + async scanQrcode(options?: IParams | undefined): Promise { + const overlay = createScanQrcode(options); + return overlay.onWillDismiss(); + } + + createQrcode(value: string, options?: IParams | undefined): IParams { + return new QRCodeStyling({ + data: unescape(encodeURIComponent(value)), + ...options, + }); + } +} diff --git a/src/util/scan-qrcode/scan-qrcode.scss b/src/util/scan-qrcode/scan-qrcode.scss new file mode 100644 index 00000000000..5fabd7d16c1 --- /dev/null +++ b/src/util/scan-qrcode/scan-qrcode.scss @@ -0,0 +1,99 @@ +$scan-qrcode: ( + 'background-image-bg': rgb(136 176 255 / 10%), + 'bg-mask': rgb(0 0 0 / 50%), +); + +@include b('scan-qrcode') { + + @include set-component-css-var('scan-qrcode', $scan-qrcode); + position: fixed; + inset: 0; + z-index: 99; + width: 100vw; + height: 100vh; + background: getCssVar('scan-qrcode', 'bg-mask'); + + @include e('scanner') { + position: absolute; + inset: 0; + z-index: 9; + width: 100%; + height: 100%; + background-color: getCssVar('scan-qrcode', 'bg-mask'); + } + + @include e('line-box') { + position: relative; + top: 50%; + left: 50%; + width: 75vw; + max-width: 75vh; + height: 75vw; + max-height: 75vh; + overflow: hidden; + border: 1px solid getCssVar(color, primary); + transform: translate(-50%, -50%); + + @include m('row') { + display: flex; + align-items: flex-end; + justify-content: center; + width: 100%; + overflow: hidden; + background-image: linear-gradient(0deg, + transparent 24%, + getCssVar('scan-qrcode', 'background-image-bg') 25%, + getCssVar('scan-qrcode', 'background-image-bg') 26%, + transparent 27%, + transparent 74%, + getCssVar('scan-qrcode', 'background-image-bg') 75%, + getCssVar('scan-qrcode', 'background-image-bg') 76%, + transparent 77%, + transparent), + linear-gradient(90deg, + transparent 24%, + getCssVar('scan-qrcode', 'background-image-bg') 25%, + getCssVar('scan-qrcode', 'background-image-bg') 26%, + transparent 27%, + transparent 74%, + getCssVar('scan-qrcode', 'background-image-bg') 75%, + getCssVar('scan-qrcode', 'background-image-bg') 76%, + transparent 77%, + transparent); + background-position: -1rem -1rem; + background-size: 3rem 3rem; + border-bottom: 1px solid getCssVar('scan-qrcode', 'background-image-bg'); + animation: height-change 2s infinite; + animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99); + animation-delay: 1.4s; + } + + @include m('line') { + width: 100%; + height: rem(3px); + background: getCssVar(color, primary); + filter: blur(rem(4px)); + } + } + + @include e('close') { + position: absolute; + top: getCssVar('spacing', 'base'); + left: getCssVar('spacing', 'base'); + color: getCssVar(color, white); + font-size: getCssVar('font-size', 'header-3'); + cursor: pointer; + } + + + + @keyframes height-change { + 0% { + height: 0; + } + + 100% { + height: 100%; + } + } +} \ No newline at end of file diff --git a/src/util/scan-qrcode/scan-qrcode.tsx b/src/util/scan-qrcode/scan-qrcode.tsx new file mode 100644 index 00000000000..f2502794692 --- /dev/null +++ b/src/util/scan-qrcode/scan-qrcode.tsx @@ -0,0 +1,160 @@ +import { defineComponent, PropType, ref } from 'vue'; +import { + OverlayContainer, + useNamespace, + useUIStore, +} from '@ibiz-template/vue3-util'; +import { QrcodeStream } from 'vue-qrcode-reader'; +import { IModalOptions, IOverlayContainer } from '@ibiz-template/runtime'; +import './scan-qrcode.scss'; + +export const ScanQrcodeComponent = defineComponent({ + components: { + QrcodeStream, + }, + props: { + opts: { + type: Object as PropType, + default: () => ({}), + }, + }, + setup(props, { emit }) { + const ns = useNamespace('scan-qrcode'); + + const { zIndex } = useUIStore(); + const overlayZIndex = zIndex.increment(); + + // 相机配置选项: 'user'|'environment' (默认:environment) + const constraintsConfig = ref({ facingMode: 'environment' }); + + /** + * 错误提示 + * + * @author ljx + * @date 2024-12-10 10:12:50 + * @param {IParams} error + * @return {*} + */ + const onError = (error: IParams) => { + let errorText = `[${error.name}]: `; + switch (error.name) { + case 'NotAllowedError': + errorText += ibiz.i18n.t('util.scanQrcode.notAllowedError'); + break; + case 'NotFoundError': + errorText += ibiz.i18n.t('util.scanQrcode.notFoundError'); + break; + case 'NotSupportedError': + errorText += ibiz.i18n.t('util.scanQrcode.notSupportedError'); + break; + case 'NotReadableError': + errorText += ibiz.i18n.t('util.scanQrcode.notReadableError'); + break; + case 'OverconstrainedError': + errorText += ibiz.i18n.t('util.scanQrcode.overconstrainedError'); + break; + case 'StreamApiNotSupportedError': + errorText += ibiz.i18n.t( + 'util.scanQrcode.streamApiNotSupportedError', + ); + break; + case 'InsecureContextError': + errorText += ibiz.i18n.t('util.scanQrcode.insecureContextError'); + break; + default: + errorText += error.message; + } + ibiz.notification.error({ desc: errorText }); + }; + + /** + * 解码完成 交给父组件处理 + * + * @author ljx + * @date 2024-12-10 10:12:50 + * @param {string} value + * @return {*} + */ + const onDecode = (value: string) => { + emit('dismiss', { ok: true, value }); + }; + + /** + * 一旦用户相机的流被加载,它就会显示出来并不断扫描二维码 + * + * @author ljx + * @date 2024-12-10 10:12:50 + * @param {IData[]} detectedCodes + * @return {*} + */ + const onDetect = (detectedCodes: IData[]) => { + if (detectedCodes.length > 0) { + onDecode(detectedCodes[0]?.rawValue); + } + }; + + /** + * 关闭弹框 + * + * @author ljx + * @date 2024-12-10 10:12:50 + * @return {*} + */ + const onClose = () => { + emit('dismiss', { ok: false, value: '' }); + }; + + return { + ns, + overlayZIndex, + constraintsConfig, + onDetect, + onError, + onClose, + }; + }, + render() { + return ( +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ ); + }, +}); + +/** + * 创建模态框 + * + * @author ljx + * @date 2024-12-10 10:12:50 + * @export + * @param {(IModalOptions | undefined)} [opts] + * @return {*} {IOverlayContainer} + */ +export function createScanQrcode( + opts?: IModalOptions | undefined, +): IOverlayContainer { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new OverlayContainer(ScanQrcodeComponent, {} as any, opts); +} -- Gitee