feat: check-in scanner and fix bugs
All checks were successful
Backend Check Build (NixCN CMS) TeamCity build finished
Client CMS Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-11 21:56:04 +08:00
parent 1a5deabadb
commit 25a2bf75c5
9 changed files with 118 additions and 5 deletions

View File

@@ -47,6 +47,7 @@
"@tanstack/zod-adapter": "^1.143.4", "@tanstack/zod-adapter": "^1.143.4",
"@tanstack/zod-form-adapter": "^0.42.1", "@tanstack/zod-form-adapter": "^0.42.1",
"@uiw/react-md-editor": "^4.0.11", "@uiw/react-md-editor": "^4.0.11",
"@yudiel/react-qr-scanner": "^2.5.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",

View File

@@ -107,6 +107,9 @@ importers:
'@uiw/react-md-editor': '@uiw/react-md-editor':
specifier: ^4.0.11 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) 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: axios:
specifier: ^1.13.2 specifier: ^1.13.2
version: 1.13.2 version: 1.13.2
@@ -2526,6 +2529,9 @@ packages:
'@types/doctrine@0.0.9': '@types/doctrine@0.0.9':
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
'@types/emscripten@1.41.5':
resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==}
'@types/estree-jsx@1.0.5': '@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -2757,6 +2763,12 @@ packages:
'@vue/shared@3.5.27': '@vue/shared@3.5.27':
resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} 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: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
@@ -2854,6 +2866,9 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
barcode-detector@3.0.8:
resolution: {integrity: sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==}
base-64@1.0.0: base-64@1.0.0:
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
@@ -4929,6 +4944,9 @@ packages:
resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==}
engines: {node: ^14.0.0 || >=16.0.0} engines: {node: ^14.0.0 || >=16.0.0}
sdp@3.2.1:
resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@@ -5405,6 +5423,10 @@ packages:
webpack-virtual-modules@0.6.2: webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} 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: which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
@@ -5526,6 +5548,11 @@ packages:
zwitch@2.0.4: zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 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: snapshots:
'@adobe/css-tools@4.4.4': {} '@adobe/css-tools@4.4.4': {}
@@ -7745,6 +7772,8 @@ snapshots:
'@types/doctrine@0.0.9': {} '@types/doctrine@0.0.9': {}
'@types/emscripten@1.41.5': {}
'@types/estree-jsx@1.0.5': '@types/estree-jsx@1.0.5':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -8091,6 +8120,15 @@ snapshots:
'@vue/shared@3.5.27': {} '@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): acorn-jsx@5.3.2(acorn@8.15.0):
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
@@ -8180,6 +8218,12 @@ snapshots:
balanced-match@1.0.2: {} 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: {} base-64@1.0.0: {}
base64-js@1.5.1: {} base64-js@1.5.1: {}
@@ -10713,6 +10757,8 @@ snapshots:
refa: 0.12.1 refa: 0.12.1
regexp-ast-analysis: 0.7.1 regexp-ast-analysis: 0.7.1
sdp@3.2.1: {}
semver@6.3.1: {} semver@6.3.1: {}
semver@7.7.3: {} semver@7.7.3: {}
@@ -11180,6 +11226,10 @@ snapshots:
webpack-virtual-modules@0.6.2: {} webpack-virtual-modules@0.6.2: {}
webrtc-adapter@9.0.3:
dependencies:
sdp: 3.2.1
which-module@2.0.1: {} which-module@2.0.1: {}
which@2.0.2: which@2.0.2:
@@ -11283,3 +11333,8 @@ snapshots:
use-sync-external-store: 1.6.0(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3)
zwitch@2.0.4: {} 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

View File

@@ -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 (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<Scanner
onScan={(result) => {
if (result.length > 0) {
onScan(result[0].rawValue);
}
}}
onError={(error) => { throw error; }}
/>
</DialogHeader>
</DialogContent>
);
}

View File

@@ -27,8 +27,8 @@ import {
import { Switch } from '../ui/switch'; import { Switch } from '../ui/switch';
const formSchema = z.object({ const formSchema = z.object({
username: z.string().min(5), username: z.string().min(5, '用户名长度至少为5个字符'),
nickname: z.string(), nickname: z.string().nonempty('昵称不能为空'),
subtitle: z.string(), subtitle: z.string(),
avatar: z.string().url().or(z.literal('')), avatar: z.string().url().or(z.literal('')),
allow_public: z.boolean(), allow_public: z.boolean(),

View File

@@ -19,7 +19,7 @@ const meta = {
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Prompt: Story = { export const Primary: Story = {
args: { args: {
checkinCode: '114514', checkinCode: '114514',
}, },

View File

@@ -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 => (
<Dialog open={true}>
<Story />
</Dialog>
),
],
} satisfies Meta<typeof CheckinScannerDialogView>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
onScan: (value) => {
// eslint-disable-next-line no-console
console.log('Scanned value:', value);
},
},
};

View File

@@ -32,6 +32,7 @@ export const Loading: Story = {
description: '', description: '',
startTime: new Date(0), startTime: new Date(0),
endTime: new Date(0), endTime: new Date(0),
isCheckedIn: false,
}, },
actionFooter: <Button className="w-full"></Button>, actionFooter: <Button className="w-full"></Button>,
}, },

View File

@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import { EventGridSkeleton } from '@/components/events/event-grid/event-grid.skeleton'; import { EventGridSkeleton } from '@/components/events/event-grid/event-grid.skeleton';
import { EventGridView } from '@/components/events/event-grid/event-grid.view'; 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 { Button } from '@/components/ui/button';
import { Skeleton as UiSkeleton } from '@/components/ui/skeleton'; import { Skeleton as UiSkeleton } from '@/components/ui/skeleton';
import { exampleMultiEvents } from './event.example'; import { exampleMultiEvents } from './event.example';
@@ -24,7 +23,12 @@ export const Primary: Story = {
export const Joined: Story = { export const Joined: Story = {
args: { args: {
events: exampleMultiEvents, events: exampleMultiEvents,
footer: event => <JoinedEventGridFooter event={event} />, footer: () => (
<div className="flex flex-row justify-between w-full gap-4">
<Button className="flex-1"></Button>
<Button className="flex-1"></Button>
</div>
),
}, },
}; };

View File

@@ -10,6 +10,7 @@ export const exampleEvent: EventInfo = {
description: 'Event Description', description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'), startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'),
isCheckedIn: false,
}; };
export const exampleMultiEvents: EventInfo[] = [ export const exampleMultiEvents: EventInfo[] = [
@@ -23,6 +24,7 @@ export const exampleMultiEvents: EventInfo[] = [
description: 'Event Description', description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'), startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'),
isCheckedIn: false,
}, },
{ {
eventId: '2', eventId: '2',
@@ -34,6 +36,7 @@ export const exampleMultiEvents: EventInfo[] = [
description: 'Event Description', description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'), startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'),
isCheckedIn: false,
}, },
{ {
eventId: '3', eventId: '3',
@@ -45,6 +48,7 @@ export const exampleMultiEvents: EventInfo[] = [
description: 'Event Description', description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'), startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'),
isCheckedIn: false,
}, },
{ {
eventId: '4', eventId: '4',
@@ -56,5 +60,6 @@ export const exampleMultiEvents: EventInfo[] = [
description: 'Event Description', description: 'Event Description',
startTime: new Date('2026-06-13T04:00:00.000Z'), startTime: new Date('2026-06-13T04:00:00.000Z'),
endTime: new Date('2026-06-14T04:00:00.000Z'), endTime: new Date('2026-06-14T04:00:00.000Z'),
isCheckedIn: false,
}, },
]; ];