feat: complete V1 phases 0-7

This commit is contained in:
2026-08-12 14:35:12 +08:00
commit c3dd9be674
311 changed files with 38750 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { ApiClientError } from './api/api-client';
import { ADMIN_APP_TITLE } from './app-info';
import { validateLoginForm } from './login-form';
import { useAuthStore } from './stores/auth';
import ServiceApplicationsView from './views/ServiceApplicationsView.vue';
import BeneficiariesView from './views/BeneficiariesView.vue';
import ServiceCasesView from './views/ServiceCasesView.vue';
import FollowUpsView from './views/FollowUpsView.vue';
import VolunteersView from './views/VolunteersView.vue';
import ProjectsView from './views/ProjectsView.vue';
import ActivitiesView from './views/ActivitiesView.vue';
import ContentView from './views/ContentView.vue';
import NotificationDeliveriesView from './views/NotificationDeliveriesView.vue';
import StatisticsView from './views/StatisticsView.vue';
const auth = useAuthStore();
const form = reactive({ username: '', password: '' });
const errors = reactive<{ username?: string; password?: string }>({});
const submitting = ref(false);
const showPassword = ref(false);
const message = ref('');
const activeView = ref<
| 'applications'
| 'beneficiaries'
| 'cases'
| 'follow-ups'
| 'volunteers'
| 'projects'
| 'activities'
| 'content'
| 'notifications'
| 'statistics'
>('applications');
onMounted(() => auth.restore());
async function submit(): Promise<void> {
message.value = '';
errors.username = undefined;
errors.password = undefined;
const validation = validateLoginForm(form.username, form.password);
Object.assign(errors, validation);
if (Object.keys(validation).length) return;
submitting.value = true;
try {
await auth.login(form.username.trim().toLowerCase(), form.password);
form.password = '';
} catch (error) {
message.value =
error instanceof ApiClientError
? `${error.message}${error.requestId ? `(编号:${error.requestId})` : ''}`
: '登录失败,请稍后重试';
} finally {
submitting.value = false;
}
}
async function logout(): Promise<void> {
submitting.value = true;
message.value = '';
try {
await auth.logout();
} catch (error) {
message.value = error instanceof Error ? error.message : '退出失败,请稍后重试';
} finally {
submitting.value = false;
}
}
</script>
<template>
<main v-if="auth.checking" class="app-shell app-shell--centered" aria-live="polite">
<section class="welcome-card">
<p class="eyebrow">乐活公益 · 暖心服务平台</p>
<h1>{{ ADMIN_APP_TITLE }}</h1>
<p>正在确认登录状态,请稍候…</p>
</section>
</main>
<main v-else-if="!auth.authenticated" class="app-shell app-shell--centered">
<section class="login-card" aria-labelledby="app-title">
<p class="eyebrow">乐活公益 · 暖心服务平台</p>
<h1 id="app-title">{{ ADMIN_APP_TITLE }}</h1>
<p class="intro">仅限经授权的工作人员使用。登录操作将记录安全审计。</p>
<form novalidate @submit.prevent="submit">
<label for="username">登录账号</label>
<input
id="username"
v-model="form.username"
name="username"
autocomplete="username"
:aria-invalid="Boolean(errors.username)"
:aria-describedby="errors.username ? 'username-error' : undefined"
/>
<p v-if="errors.username" id="username-error" class="field-error" role="alert">
{{ errors.username }}
</p>
<label for="password">登录密码</label>
<div class="password-field">
<input
id="password"
v-model="form.password"
name="password"
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
:aria-invalid="Boolean(errors.password)"
:aria-describedby="errors.password ? 'password-error' : undefined"
/>
<button
type="button"
class="password-toggle"
:aria-label="showPassword ? '隐藏密码' : '显示密码'"
@click="showPassword = !showPassword"
>
{{ showPassword ? '隐藏' : '显示' }}
</button>
</div>
<p v-if="errors.password" id="password-error" class="field-error" role="alert">
{{ errors.password }}
</p>
<p v-if="message" class="form-message" role="alert">{{ message }}</p>
<button class="primary-button" type="submit" :disabled="submitting">
{{ submitting ? '正在安全登录…' : '登录管理后台' }}
</button>
</form>
</section>
</main>
<div v-else class="admin-layout">
<aside class="admin-sidebar" aria-label="管理后台导航">
<div class="brand-block">
<span class="brand-mark" aria-hidden="true"></span>
<div><strong>乐活公益</strong><small>管理平台</small></div>
</div>
<nav>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'applications' }]"
type="button"
:aria-current="activeView === 'applications' ? 'page' : undefined"
@click="activeView = 'applications'"
>
服务申请
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'statistics' }]"
type="button"
:aria-current="activeView === 'statistics' ? 'page' : undefined"
@click="activeView = 'statistics'"
>
服务统计
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'content' }]"
type="button"
:aria-current="activeView === 'content' ? 'page' : undefined"
@click="activeView = 'content'"
>
内容发布
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'notifications' }]"
type="button"
:aria-current="activeView === 'notifications' ? 'page' : undefined"
@click="activeView = 'notifications'"
>
通知投递
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'beneficiaries' }]"
type="button"
:aria-current="activeView === 'beneficiaries' ? 'page' : undefined"
@click="activeView = 'beneficiaries'"
>
服务对象
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'cases' }]"
type="button"
:aria-current="activeView === 'cases' ? 'page' : undefined"
@click="activeView = 'cases'"
>
服务工单
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'follow-ups' }]"
type="button"
:aria-current="activeView === 'follow-ups' ? 'page' : undefined"
@click="activeView = 'follow-ups'"
>
回访工作台
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'volunteers' }]"
type="button"
:aria-current="activeView === 'volunteers' ? 'page' : undefined"
@click="activeView = 'volunteers'"
>
志愿者审核
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'projects' }]"
type="button"
:aria-current="activeView === 'projects' ? 'page' : undefined"
@click="activeView = 'projects'"
>
公益项目
</button>
<button
:class="['nav-item', { 'nav-item--active': activeView === 'activities' }]"
type="button"
:aria-current="activeView === 'activities' ? 'page' : undefined"
@click="activeView = 'activities'"
>
活动与报名
</button>
</nav>
<p class="phase-note">Phase 6:内容审核、站内消息与微信订阅通知</p>
</aside>
<div class="admin-main">
<header class="admin-topbar">
<div>
<strong>{{ auth.user?.displayName ?? '工作人员' }}</strong>
<span>已安全登录</span>
</div>
<button class="text-button" type="button" :disabled="submitting" @click="logout">
{{ submitting ? '正在退出…' : '安全退出' }}
</button>
</header>
<p v-if="message" class="global-message" role="alert">{{ message }}</p>
<ServiceApplicationsView v-if="activeView === 'applications'" />
<BeneficiariesView v-else-if="activeView === 'beneficiaries'" />
<ServiceCasesView v-else-if="activeView === 'cases'" />
<FollowUpsView v-else-if="activeView === 'follow-ups'" />
<VolunteersView v-else-if="activeView === 'volunteers'" />
<ProjectsView v-else-if="activeView === 'projects'" />
<ActivitiesView v-else-if="activeView === 'activities'" />
<ContentView v-else-if="activeView === 'content'" />
<NotificationDeliveriesView v-else-if="activeView === 'notifications'" />
<StatisticsView v-else />
</div>
</div>
</template>