From 7fea6b8578c1cb930ea286d3ccafcb6e33852312 Mon Sep 17 00:00:00 2001 From: empty Date: Tue, 3 Feb 2026 23:52:29 +0800 Subject: [PATCH] feat(admin): enhance program config with CRUD and reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change "结果展示" to "投票结果" in global controls - Add program edit modal with name, team, performer, remark fields - Add move up/down buttons for program reordering - Add delete program with confirmation - Add "添加节目" button for creating new programs - Add responsive styles for mobile view Co-Authored-By: Claude Opus 4.5 --- packages/client-screen/src/router/index.ts | 2 +- .../client-screen/src/views/AdminControl.vue | 458 +++++++++++++++++- .../src/views/VoteResultsView.vue | 2 +- 3 files changed, 453 insertions(+), 9 deletions(-) diff --git a/packages/client-screen/src/router/index.ts b/packages/client-screen/src/router/index.ts index 212a997..1083a5d 100644 --- a/packages/client-screen/src/router/index.ts +++ b/packages/client-screen/src/router/index.ts @@ -43,7 +43,7 @@ const router = createRouter({ path: '/screen/results', name: 'screen-results', component: () => import('../views/VoteResultsView.vue'), - meta: { title: '年会大屏 - 投票结果' }, + meta: { title: '年会大屏 - 结果展示' }, }, { path: '/screen/lottery-results', diff --git a/packages/client-screen/src/views/AdminControl.vue b/packages/client-screen/src/views/AdminControl.vue index 93dc5c6..f9ab44f 100644 --- a/packages/client-screen/src/views/AdminControl.vue +++ b/packages/client-screen/src/views/AdminControl.vue @@ -77,6 +77,18 @@ function showToast(message: string, type: 'error' | 'success' | 'info' = 'info', }, duration); } +// Program editing state +const showProgramModal = ref(false); +const editingProgram = ref(null); +const programForm = ref({ + id: '', + name: '', + teamName: '', + performer: '', + remark: '', +}); +const programSaving = ref(false); + // Prize configuration state const showPrizeConfig = ref(false); const prizeConfigLoading = ref(false); @@ -255,9 +267,132 @@ async function loadParticipants() { } } -// Navigation -function goBack() { - router.push('/'); +// Program management functions +function openProgramEdit(program: any) { + editingProgram.value = program; + programForm.value = { + id: program.id, + name: program.name || '', + teamName: program.teamName || '', + performer: program.performer || '', + remark: program.remark || '', + }; + showProgramModal.value = true; +} + +function openAddProgram() { + editingProgram.value = null; + programForm.value = { + id: '', + name: '', + teamName: '', + performer: '', + remark: '', + }; + showProgramModal.value = true; +} + +function closeProgramModal() { + showProgramModal.value = false; + editingProgram.value = null; +} + +async function saveProgramsToServer(programs: any[]) { + const res = await fetch('/api/admin/programs', { + method: 'PUT', + headers: getAdminHeaders(), + body: JSON.stringify({ programs }), + }); + const data = await readJsonSafe(res); + if (!res.ok) { + throw new Error(data?.error || data?.message || `保存节目配置失败(${res.status})`); + } + return data; +} + +async function saveProgram() { + programSaving.value = true; + try { + const programs = [...admin.programs]; + if (editingProgram.value) { + // Edit existing program + const index = programs.findIndex(p => p.id === editingProgram.value.id); + if (index !== -1) { + programs[index] = { + ...programs[index], + name: programForm.value.name, + teamName: programForm.value.teamName, + performer: programForm.value.performer, + remark: programForm.value.remark, + }; + } + } else { + // Add new program + const newId = `program_${Date.now()}`; + programs.push({ + id: newId, + name: programForm.value.name, + teamName: programForm.value.teamName, + performer: programForm.value.performer, + remark: programForm.value.remark, + status: 'pending', + order: programs.length + 1, + }); + } + + await saveProgramsToServer(programs); + showToast(editingProgram.value ? '节目已更新' : '节目已添加', 'success'); + closeProgramModal(); + } catch (e) { + const message = (e as Error).message; + showToast(`保存失败: ${message}`, 'error'); + } finally { + programSaving.value = false; + } +} + +async function deleteProgram(programId: string) { + if (!confirm('确定要删除这个节目吗?')) return; + + try { + const programs = admin.programs.filter(p => p.id !== programId); + await saveProgramsToServer(programs); + showToast('节目已删除', 'success'); + } catch (e) { + const message = (e as Error).message; + showToast(`删除失败: ${message}`, 'error'); + } +} + +async function moveProgram(programId: string, direction: 'up' | 'down') { + const programs = [...admin.programs]; + const index = programs.findIndex(p => p.id === programId); + if (index === -1) return; + + const newIndex = direction === 'up' ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= programs.length) return; + + // Swap positions + [programs[index], programs[newIndex]] = [programs[newIndex], programs[index]]; + + // Update order field + programs.forEach((p, i) => { + p.order = i + 1; + }); + + try { + await saveProgramsToServer(programs); + showToast('顺序已调整', 'success'); + } catch (e) { + const message = (e as Error).message; + showToast(`调整失败: ${message}`, 'error'); + } +} + +// Logout and go to login page +function handleLogout() { + localStorage.removeItem(ADMIN_TOKEN_KEY); + router.push('/admin/login'); } // Phase control @@ -551,7 +686,7 @@ onMounted(() => {
- +

管理控制台

@@ -862,6 +997,53 @@ onMounted(() => {
+ +
+ +

🎬 节目配置

+ +
+
+
+ 暂无节目数据 +
+
+
+ {{ index + 1 }} + {{ program.name }} + {{ program.teamName }} + + {{ program.status === 'current' ? '进行中' : program.status === 'completed' ? '已完成' : '待演出' }} + +
+ + + + +
+
+
+
+
+
@@ -899,7 +1081,7 @@ onMounted(() => { :class="{ active: admin.systemPhase === 'RESULTS' }" @click="setPhase('RESULTS')" > - 结果展示 + 投票结果 +
+ + +
+ @@ -2090,11 +2310,40 @@ $admin-danger: #ef4444; .control-section { &.lottery-section, &.global-section, - &.stats-section { + &.stats-section, + &.program-section { grid-column: span 1; } } + // Program Section Mobile + .program-config-item { + grid-template-columns: 32px 1fr 80px; + gap: 8px; + padding: 10px 12px; + + .program-team { + display: none; + } + + .program-status { + font-size: 11px; + padding: 3px 8px; + } + } + + .program-actions { + grid-column: span 3; + justify-content: flex-end; + margin-top: 8px; + } + + .program-order { + width: 24px; + height: 24px; + font-size: 12px; + } + // Stats Section Mobile .award-stats-grid { grid-template-columns: 1fr; @@ -2484,4 +2733,199 @@ $admin-danger: #ef4444; color: #888; padding: 20px; } + +// Program Configuration Styles +.program-section { + grid-column: span 2; +} + +.add-program-btn { + padding: 6px 12px; + background: $admin-primary; + border: none; + border-radius: 6px; + color: white; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: lighten($admin-primary, 5%); + } +} + +.program-config-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.program-config-item { + display: grid; + grid-template-columns: 40px 1fr 120px 80px auto; + gap: 12px; + align-items: center; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid $admin-border; + border-radius: 8px; + transition: background 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.04); + } +} + +.program-order { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: rgba($color-gold, 0.2); + color: $color-gold; + border-radius: 6px; + font-size: 13px; + font-weight: 600; +} + +.program-name { + font-size: 14px; + font-weight: 500; + color: $admin-text; +} + +.program-team { + font-size: 13px; + color: $admin-text-muted; +} + +.program-status { + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + text-align: center; + + &.pending { + background: rgba(#888, 0.2); + color: #888; + } + + &.current { + background: rgba($admin-warning, 0.2); + color: $admin-warning; + } + + &.completed { + background: rgba($admin-success, 0.2); + color: $admin-success; + } +} + +.program-actions { + display: flex; + gap: 6px; +} + +.action-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: #252525; + border: 1px solid $admin-border; + border-radius: 6px; + color: $admin-text-muted; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + + &:hover:not(:disabled) { + background: #333; + color: $admin-text; + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &.edit:hover:not(:disabled) { + border-color: $admin-primary; + color: $admin-primary; + } + + &.delete:hover:not(:disabled) { + border-color: $admin-danger; + color: $admin-danger; + } +} + +// Program Modal Styles +.program-modal { + width: 450px; + max-width: 90vw; + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + + h3 { + margin: 0; + font-size: 18px; + } + + .close-btn { + background: none; + border: none; + color: $admin-text-muted; + font-size: 24px; + cursor: pointer; + padding: 0; + line-height: 1; + + &:hover { + color: $admin-text; + } + } + } + + .modal-body { + display: flex; + flex-direction: column; + gap: 16px; + } + + .form-group { + display: flex; + flex-direction: column; + gap: 6px; + + label { + font-size: 13px; + color: $admin-text-muted; + } + + input { + padding: 10px 12px; + background: #252525; + border: 1px solid $admin-border; + border-radius: 6px; + color: $admin-text; + font-size: 14px; + + &:focus { + outline: none; + border-color: $admin-primary; + } + + &::placeholder { + color: #555; + } + } + } +} diff --git a/packages/client-screen/src/views/VoteResultsView.vue b/packages/client-screen/src/views/VoteResultsView.vue index d6476b0..8fb9278 100644 --- a/packages/client-screen/src/views/VoteResultsView.vue +++ b/packages/client-screen/src/views/VoteResultsView.vue @@ -81,7 +81,7 @@ onMounted(() => {
-

投票结果

+

结果展示

{{ admin.isConnected ? '已连接' : '连接中...' }}