feat: complete V1 phases 0-7
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user