diff --git a/package-lock.json b/package-lock.json index b1ec0b3..4c6ad8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -3694,7 +3693,6 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3715,7 +3713,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3726,7 +3723,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3788,7 +3784,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -4143,7 +4138,6 @@ "integrity": "sha512-hRDjg6dlDz7JlZAvjbiCdAJ3SDG+NH8tjZe21vjxfvT2ssYAn72SRXMge3dKKABm3bIJ3C+3wdunIdur8PHEAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.17", "fflate": "^0.8.2", @@ -4180,7 +4174,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4348,7 +4341,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4870,8 +4862,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -5053,7 +5044,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6875,7 +6865,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -7512,8 +7501,7 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -7931,7 +7919,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7962,7 +7949,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7975,7 +7961,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7999,7 +7984,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -8141,8 +8125,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8395,7 +8378,6 @@ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -8817,7 +8799,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8862,7 +8843,6 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -9149,7 +9129,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9242,7 +9221,6 @@ "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", @@ -9407,7 +9385,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/lib/types.ts b/src/lib/types.ts index f2c38f5..29737ba 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,7 +3,7 @@ export interface Participant { name: string; dept: string; mustWinPrizeId: string | null; // 内定中特定奖项 ID,null 为无内定 - banned: boolean; + bannedPrizes: string[]; // 奖项黑名单:不能中这些奖项的ID数组 weight: number; } diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx index 8cf71a0..f1fb4f5 100644 --- a/src/pages/AdminPage.tsx +++ b/src/pages/AdminPage.tsx @@ -9,8 +9,9 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Switch } from "@/components/ui/switch"; +import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Download, Upload, Trash, Play, Square, ShieldAlert, MonitorPlay, LogOut, @@ -62,7 +63,7 @@ export default function AdminPage() { // Person Edit State const [editingPerson, setEditingPerson] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [editForm, setEditForm] = useState({ name: "", dept: "", mustWinPrizeId: "none", banned: false, weight: 1 }); + const [editForm, setEditForm] = useState({ name: "", dept: "", mustWinPrizeId: "none", bannedPrizes: [] as string[], weight: 1 }); // Prize Edit State const [editingPrize, setEditingPrize] = useState(null); @@ -280,7 +281,7 @@ export default function AdminPage() { const openAddDialog = () => { setEditingPerson(null); - setEditForm({ name: "", dept: "", mustWinPrizeId: "none", banned: false, weight: 1 }); + setEditForm({ name: "", dept: "", mustWinPrizeId: "none", bannedPrizes: [], weight: 1 }); setIsDialogOpen(true); }; @@ -290,7 +291,7 @@ export default function AdminPage() { name: person.name, dept: person.dept, mustWinPrizeId: person.mustWinPrizeId || "none", - banned: person.banned, + bannedPrizes: person.bannedPrizes, weight: person.weight }); setIsDialogOpen(true); @@ -425,7 +426,7 @@ export default function AdminPage() { p.name.includes(searchTerm) || p.dept.includes(searchTerm) ); const winnerIds = new Set(winners.map(w => w.id)); - const validPool = participants.filter(p => !winnerIds.has(p.id) && !p.banned); + const validPool = participants.filter(p => !winnerIds.has(p.id) && (currentPrizeId ? !p.bannedPrizes.includes(currentPrizeId) : true)); const finalPool = currentPrizeId ? validPool.filter(p => !p.mustWinPrizeId || p.mustWinPrizeId === currentPrizeId) : []; @@ -634,7 +635,7 @@ export default function AdminPage() {
禁中: - {participants.filter(p=>p.banned).length} + {participants.filter(p=>p.bannedPrizes.length > 0).length}
@@ -677,7 +678,14 @@ export default function AdminPage() { {p.mustWinPrizeId ? {prizeName || '未知奖项'} : -} - {p.banned ? 禁止 : -} + {p.bannedPrizes.length > 0 ? ( +
+ {p.bannedPrizes.map(id => { + const prize = prizes.find(pr => pr.id === id); + return prize ? {prize.name} : null; + })} +
+ ) : -}
{p.weight} @@ -849,12 +857,61 @@ export default function AdminPage() {
- +
-
- setEditForm({...editForm, banned: c})} /> - -
+ + + + + +
+ +
+ {prizes.map(prize => ( +
+ { + if (checked) { + setEditForm({...editForm, bannedPrizes: [...editForm.bannedPrizes, prize.id]}); + } else { + setEditForm({...editForm, bannedPrizes: editForm.bannedPrizes.filter(id => id !== prize.id)}); + } + }} + /> + +
+ ))} +
+
+
+
diff --git a/src/pages/DisplayPage.tsx b/src/pages/DisplayPage.tsx index dbc270b..222150b 100644 --- a/src/pages/DisplayPage.tsx +++ b/src/pages/DisplayPage.tsx @@ -25,7 +25,7 @@ export default function DisplayPage() { // 当前奖项信息 const currentPrize = prizes.find(p => p.id === currentPrizeId); - const candidates = participants.filter(p => !p.banned); + const candidates = participants.filter(p => currentPrizeId ? !p.bannedPrizes.includes(currentPrizeId) : true); // 监听 Store 变化 (仅保留状态更新,不再播放音效) useEffect(() => { diff --git a/src/store/useStore.ts b/src/store/useStore.ts index f808594..c74e599 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -79,7 +79,7 @@ export const useLotteryStore = create()( name: row['姓名'] || row['name'] || 'Unknown', dept: row['部门'] || row['dept'] || '', mustWinPrizeId: null, - banned: false, + bannedPrizes: [], weight: 1, }; @@ -90,7 +90,15 @@ export const useLotteryStore = create()( const prize = currentPrizes.find(p => p.name === prizeName); if (prize) participant.mustWinPrizeId = prize.id; } - participant.banned = (row['禁止中奖(是/否)'] === '是' || row['banned'] === 'true'); + // 解析奖项黑名单:逗号分隔的奖项名称 + const bannedPrizeNames = row['禁止奖项'] || row['bannedPrizes'] || ''; + if (bannedPrizeNames) { + const names = bannedPrizeNames.split(',').map((n: string) => n.trim()); + participant.bannedPrizes = names.map((name: string) => { + const prize = currentPrizes.find(p => p.name === name); + return prize ? prize.id : ''; + }).filter(id => id); + } participant.weight = parseInt(row['权重(1-10)'] || row['weight'] || '1') || 1; } @@ -135,7 +143,7 @@ export const useLotteryStore = create()( } const winnerIds = new Set(winners.map(w => w.id)); - const validPool = participants.filter(p => !winnerIds.has(p.id) && !p.banned); + const validPool = participants.filter(p => !winnerIds.has(p.id) && !p.bannedPrizes.includes(currentPrizeId)); const finalPool = validPool.filter(p => !p.mustWinPrizeId || p.mustWinPrizeId === currentPrizeId); if (finalPool.length === 0) { set({ isRolling: false, roundWinners: [] }); @@ -161,11 +169,11 @@ export const useLotteryStore = create()( // 1. 排除历史已中奖 const winnerIds = new Set(winners.map(w => w.id)); - // 2. 排除黑名单 - const validPool = participants.filter(p => !winnerIds.has(p.id) && !p.banned); + // 2. 排除奖项黑名单 + const validPool = participants.filter(p => !winnerIds.has(p.id) && !p.bannedPrizes.includes(currentPrizeId)); // 3. 找出当前奖项的内定者 const mustWinCandidates = participants.filter(p => - p.mustWinPrizeId === currentPrizeId && !winnerIds.has(p.id) && !p.banned + p.mustWinPrizeId === currentPrizeId && !winnerIds.has(p.id) && !p.bannedPrizes.includes(currentPrizeId) ); // 4. 执行算法 diff --git a/tsconfig.app.json b/tsconfig.app.json index ad71f58..b50b1b0 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -25,7 +25,6 @@ "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "baseUrl": ".", "paths": { "@/*": [ "./src/*"