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,
},
];