From 25a2bf75c583ddcbef00a5caf7f3cb75edcf49fe Mon Sep 17 00:00:00 2001 From: Noa Virellia Date: Wed, 11 Feb 2026 21:56:04 +0800 Subject: [PATCH] feat: check-in scanner and fix bugs Signed-off-by: Noa Virellia --- client/cms/package.json | 1 + client/cms/pnpm-lock.yaml | 55 +++++++++++++++++++ .../checkin/checkin-scanner.dialog.view.tsx | 20 +++++++ .../profile/edit-profile.dialog.view.tsx | 4 +- .../stories/events/checkin-dialog.stories.tsx | 2 +- .../events/checkin-scanner.stories.tsx | 27 +++++++++ .../src/stories/events/event-card.stories.tsx | 1 + .../src/stories/events/event-grid.stories.tsx | 8 ++- .../cms/src/stories/events/event.example.ts | 5 ++ 9 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 client/cms/src/components/checkin/checkin-scanner.dialog.view.tsx create mode 100644 client/cms/src/stories/events/checkin-scanner.stories.tsx diff --git a/client/cms/package.json b/client/cms/package.json index 64f3e2a..1ed41a6 100644 --- a/client/cms/package.json +++ b/client/cms/package.json @@ -47,6 +47,7 @@ "@tanstack/zod-adapter": "^1.143.4", "@tanstack/zod-form-adapter": "^0.42.1", "@uiw/react-md-editor": "^4.0.11", + "@yudiel/react-qr-scanner": "^2.5.1", "axios": "^1.13.2", "base-64": "^1.0.0", "buffer": "^6.0.3", diff --git a/client/cms/pnpm-lock.yaml b/client/cms/pnpm-lock.yaml index 669b020..ece0349 100644 --- a/client/cms/pnpm-lock.yaml +++ b/client/cms/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@uiw/react-md-editor': specifier: ^4.0.11 version: 4.0.11(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@yudiel/react-qr-scanner': + specifier: ^2.5.1 + version: 2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) axios: specifier: ^1.13.2 version: 1.13.2 @@ -2526,6 +2529,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -2757,6 +2763,12 @@ packages: '@vue/shared@3.5.27': resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + '@yudiel/react-qr-scanner@2.5.1': + resolution: {integrity: sha512-FWzHaCneu30mQpE80VNWx4IPtBjXFEiTzhwWunZy3afvvAy/x0aVIgYijJKEbROoaAeDfcJ/gIyUCqPBP7bMOw==} + peerDependencies: + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2854,6 +2866,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + barcode-detector@3.0.8: + resolution: {integrity: sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==} + base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -4929,6 +4944,9 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + sdp@3.2.1: + resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5405,6 +5423,10 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webrtc-adapter@9.0.3: + resolution: {integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} @@ -5526,6 +5548,11 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + zxing-wasm@2.2.4: + resolution: {integrity: sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==} + peerDependencies: + '@types/emscripten': '>=1.39.6' + snapshots: '@adobe/css-tools@4.4.4': {} @@ -7745,6 +7772,8 @@ snapshots: '@types/doctrine@0.0.9': {} + '@types/emscripten@1.41.5': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -8091,6 +8120,15 @@ snapshots: '@vue/shared@3.5.27': {} + '@yudiel/react-qr-scanner@2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + barcode-detector: 3.0.8(@types/emscripten@1.41.5) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + webrtc-adapter: 9.0.3 + transitivePeerDependencies: + - '@types/emscripten' + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8180,6 +8218,12 @@ snapshots: balanced-match@1.0.2: {} + barcode-detector@3.0.8(@types/emscripten@1.41.5): + dependencies: + zxing-wasm: 2.2.4(@types/emscripten@1.41.5) + transitivePeerDependencies: + - '@types/emscripten' + base-64@1.0.0: {} base64-js@1.5.1: {} @@ -10713,6 +10757,8 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + sdp@3.2.1: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -11180,6 +11226,10 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webrtc-adapter@9.0.3: + dependencies: + sdp: 3.2.1 + which-module@2.0.1: {} which@2.0.2: @@ -11283,3 +11333,8 @@ snapshots: use-sync-external-store: 1.6.0(react@19.2.3) zwitch@2.0.4: {} + + zxing-wasm@2.2.4(@types/emscripten@1.41.5): + dependencies: + '@types/emscripten': 1.41.5 + type-fest: 5.4.1 diff --git a/client/cms/src/components/checkin/checkin-scanner.dialog.view.tsx b/client/cms/src/components/checkin/checkin-scanner.dialog.view.tsx new file mode 100644 index 0000000..fcdad1a --- /dev/null +++ b/client/cms/src/components/checkin/checkin-scanner.dialog.view.tsx @@ -0,0 +1,20 @@ +import { Scanner } from '@yudiel/react-qr-scanner'; +import { DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; + +export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) => void }) { + return ( + + + 扫描签到码 + { + if (result.length > 0) { + onScan(result[0].rawValue); + } + }} + onError={(error) => { throw error; }} + /> + + + ); +} diff --git a/client/cms/src/components/profile/edit-profile.dialog.view.tsx b/client/cms/src/components/profile/edit-profile.dialog.view.tsx index ed4f5dd..897205e 100644 --- a/client/cms/src/components/profile/edit-profile.dialog.view.tsx +++ b/client/cms/src/components/profile/edit-profile.dialog.view.tsx @@ -27,8 +27,8 @@ import { import { Switch } from '../ui/switch'; const formSchema = z.object({ - username: z.string().min(5), - nickname: z.string(), + username: z.string().min(5, '用户名长度至少为5个字符'), + nickname: z.string().nonempty('昵称不能为空'), subtitle: z.string(), avatar: z.string().url().or(z.literal('')), allow_public: z.boolean(), diff --git a/client/cms/src/stories/events/checkin-dialog.stories.tsx b/client/cms/src/stories/events/checkin-dialog.stories.tsx index eaa2790..0cf6480 100644 --- a/client/cms/src/stories/events/checkin-dialog.stories.tsx +++ b/client/cms/src/stories/events/checkin-dialog.stories.tsx @@ -19,7 +19,7 @@ const meta = { export default meta; type Story = StoryObj; -export const Prompt: Story = { +export const Primary: Story = { args: { checkinCode: '114514', }, diff --git a/client/cms/src/stories/events/checkin-scanner.stories.tsx b/client/cms/src/stories/events/checkin-scanner.stories.tsx new file mode 100644 index 0000000..ead25bb --- /dev/null +++ b/client/cms/src/stories/events/checkin-scanner.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { CheckinScannerDialogView } from '@/components/checkin/checkin-scanner.dialog.view'; +import { Dialog } from '@/components/ui/dialog'; + +const meta = { + title: 'Events/CheckinScannerDialog', + component: CheckinScannerDialogView, + decorators: [ + Story => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + onScan: (value) => { + // eslint-disable-next-line no-console + console.log('Scanned value:', value); + }, + }, +}; diff --git a/client/cms/src/stories/events/event-card.stories.tsx b/client/cms/src/stories/events/event-card.stories.tsx index a263fbc..fa3de63 100644 --- a/client/cms/src/stories/events/event-card.stories.tsx +++ b/client/cms/src/stories/events/event-card.stories.tsx @@ -32,6 +32,7 @@ export const Loading: Story = { description: '', startTime: new Date(0), endTime: new Date(0), + isCheckedIn: false, }, actionFooter: , }, diff --git a/client/cms/src/stories/events/event-grid.stories.tsx b/client/cms/src/stories/events/event-grid.stories.tsx index 290130b..746bd5b 100644 --- a/client/cms/src/stories/events/event-grid.stories.tsx +++ b/client/cms/src/stories/events/event-grid.stories.tsx @@ -1,7 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { EventGridSkeleton } from '@/components/events/event-grid/event-grid.skeleton'; import { EventGridView } from '@/components/events/event-grid/event-grid.view'; -import { JoinedEventGridFooter } from '@/components/events/joined-events.containers'; import { Button } from '@/components/ui/button'; import { Skeleton as UiSkeleton } from '@/components/ui/skeleton'; import { exampleMultiEvents } from './event.example'; @@ -24,7 +23,12 @@ export const Primary: Story = { export const Joined: Story = { args: { events: exampleMultiEvents, - footer: event => , + footer: () => ( +
+ + +
+ ), }, }; diff --git a/client/cms/src/stories/events/event.example.ts b/client/cms/src/stories/events/event.example.ts index 04001e6..c9efa8d 100644 --- a/client/cms/src/stories/events/event.example.ts +++ b/client/cms/src/stories/events/event.example.ts @@ -10,6 +10,7 @@ export const exampleEvent: EventInfo = { description: 'Event Description', startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), + isCheckedIn: false, }; export const exampleMultiEvents: EventInfo[] = [ @@ -23,6 +24,7 @@ export const exampleMultiEvents: EventInfo[] = [ description: 'Event Description', startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), + isCheckedIn: false, }, { eventId: '2', @@ -34,6 +36,7 @@ export const exampleMultiEvents: EventInfo[] = [ description: 'Event Description', startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), + isCheckedIn: false, }, { eventId: '3', @@ -45,6 +48,7 @@ export const exampleMultiEvents: EventInfo[] = [ description: 'Event Description', startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), + isCheckedIn: false, }, { eventId: '4', @@ -56,5 +60,6 @@ export const exampleMultiEvents: EventInfo[] = [ description: 'Event Description', startTime: new Date('2026-06-13T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'), + isCheckedIn: false, }, ];