feat: check-in scanner and fix bugs
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
@@ -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",
|
||||
|
||||
55
client/cms/pnpm-lock.yaml
generated
55
client/cms/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -19,7 +19,7 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Prompt: Story = {
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
checkinCode: '114514',
|
||||
},
|
||||
|
||||
27
client/cms/src/stories/events/checkin-scanner.stories.tsx
Normal file
27
client/cms/src/stories/events/checkin-scanner.stories.tsx
Normal 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);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -32,6 +32,7 @@ export const Loading: Story = {
|
||||
description: '',
|
||||
startTime: new Date(0),
|
||||
endTime: new Date(0),
|
||||
isCheckedIn: false,
|
||||
},
|
||||
actionFooter: <Button className="w-full">加入活动(示例)</Button>,
|
||||
},
|
||||
|
||||
@@ -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 => <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>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user