feat(ui): restore reference mini program and admin design
This commit is contained in:
@@ -15,6 +15,8 @@ import ActivitiesView from './views/ActivitiesView.vue';
|
||||
import ContentView from './views/ContentView.vue';
|
||||
import NotificationDeliveriesView from './views/NotificationDeliveriesView.vue';
|
||||
import StatisticsView from './views/StatisticsView.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
import AdminIcon from './components/AdminIcon.vue';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const form = reactive({ username: '', password: '' });
|
||||
@@ -23,6 +25,7 @@ const submitting = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const message = ref('');
|
||||
const activeView = ref<
|
||||
| 'dashboard'
|
||||
| 'applications'
|
||||
| 'beneficiaries'
|
||||
| 'cases'
|
||||
@@ -33,7 +36,7 @@ const activeView = ref<
|
||||
| 'content'
|
||||
| 'notifications'
|
||||
| 'statistics'
|
||||
>('applications');
|
||||
>('dashboard');
|
||||
|
||||
onMounted(() => auth.restore());
|
||||
|
||||
@@ -136,13 +139,21 @@ async function logout(): Promise<void> {
|
||||
<div><strong>乐活公益</strong><small>管理平台</small></div>
|
||||
</div>
|
||||
<nav>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'dashboard' }]"
|
||||
type="button"
|
||||
:aria-current="activeView === 'dashboard' ? 'page' : undefined"
|
||||
@click="activeView = 'dashboard'"
|
||||
>
|
||||
<AdminIcon name="dashboard" />工作概览
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'applications' }]"
|
||||
type="button"
|
||||
:aria-current="activeView === 'applications' ? 'page' : undefined"
|
||||
@click="activeView = 'applications'"
|
||||
>
|
||||
服务申请
|
||||
<AdminIcon name="application" />服务申请
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'statistics' }]"
|
||||
@@ -150,7 +161,7 @@ async function logout(): Promise<void> {
|
||||
:aria-current="activeView === 'statistics' ? 'page' : undefined"
|
||||
@click="activeView = 'statistics'"
|
||||
>
|
||||
服务统计
|
||||
<AdminIcon name="chart" />服务统计
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'content' }]"
|
||||
@@ -158,7 +169,7 @@ async function logout(): Promise<void> {
|
||||
:aria-current="activeView === 'content' ? 'page' : undefined"
|
||||
@click="activeView = 'content'"
|
||||
>
|
||||
内容发布
|
||||
<AdminIcon name="content" />内容发布
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'notifications' }]"
|
||||
@@ -166,7 +177,7 @@ async function logout(): Promise<void> {
|
||||
:aria-current="activeView === 'notifications' ? 'page' : undefined"
|
||||
@click="activeView = 'notifications'"
|
||||
>
|
||||
通知投递
|
||||
<AdminIcon name="notification" />通知投递
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'beneficiaries' }]"
|
||||
@@ -174,7 +185,7 @@ async function logout(): Promise<void> {
|
||||
:aria-current="activeView === 'beneficiaries' ? 'page' : undefined"
|
||||
@click="activeView = 'beneficiaries'"
|
||||
>
|
||||
服务对象
|
||||
<AdminIcon name="people" />服务对象
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'cases' }]"
|
||||
@@ -182,7 +193,7 @@ async function logout(): Promise<void> {
|
||||
:aria-current="activeView === 'cases' ? 'page' : undefined"
|
||||
@click="activeView = 'cases'"
|
||||
>
|
||||
服务工单
|
||||
<AdminIcon name="case" />服务工单
|
||||
</button>
|
||||
<button
|
||||
:class="['nav-item', { 'nav-item--active': activeView === 'follow-ups' }]"
|
||||
@@ -217,7 +228,7 @@ async function logout(): Promise<void> {
|
||||
活动与报名
|
||||
</button>
|
||||
</nav>
|
||||
<p class="phase-note">Phase 6:内容审核、站内消息与微信订阅通知</p>
|
||||
<p class="phase-note">仅限授权人员访问,关键业务操作均记录审计。</p>
|
||||
</aside>
|
||||
<div class="admin-main">
|
||||
<header class="admin-topbar">
|
||||
@@ -230,7 +241,11 @@ async function logout(): Promise<void> {
|
||||
</button>
|
||||
</header>
|
||||
<p v-if="message" class="global-message" role="alert">{{ message }}</p>
|
||||
<ServiceApplicationsView v-if="activeView === 'applications'" />
|
||||
<DashboardView
|
||||
v-if="activeView === 'dashboard'"
|
||||
@navigate="activeView = $event as typeof activeView"
|
||||
/>
|
||||
<ServiceApplicationsView v-else-if="activeView === 'applications'" />
|
||||
<BeneficiariesView v-else-if="activeView === 'beneficiaries'" />
|
||||
<ServiceCasesView v-else-if="activeView === 'cases'" />
|
||||
<FollowUpsView v-else-if="activeView === 'follow-ups'" />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ name: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="admin-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
v-if="name === 'dashboard'"
|
||||
d="M4 4h6v6H4V4Zm10 0h6v9h-6V4ZM4 14h6v6H4v-6Zm10 3h6v3h-6v-3Z"
|
||||
/>
|
||||
<path
|
||||
v-else-if="name === 'application'"
|
||||
d="M7 3h10v3h3v15H4V6h3V3Zm2 2v2h6V5H9Zm0 6v2h6v-2H9Zm0 4v2h6v-2H9Z"
|
||||
/>
|
||||
<path
|
||||
v-else-if="name === 'people'"
|
||||
d="M8 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm8-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6ZM2 21v-3c0-3.2 2.7-5 6-5s6 1.8 6 5v3H2Zm13 0v-3.2c0-1.4-.5-2.7-1.4-3.7.8-.4 1.6-.6 2.4-.6 3 0 5 1.6 5 4.5v3h-6Z"
|
||||
/>
|
||||
<path
|
||||
v-else-if="name === 'case'"
|
||||
d="M9 3h6l1 3h5v14H3V6h5l1-3Zm1.4 3h3.2l-.3-1h-2.6l-.3 1ZM7 11v2h10v-2H7Zm0 4v2h7v-2H7Z"
|
||||
/>
|
||||
<path v-else-if="name === 'chart'" d="M4 20V9h4v11H4Zm6 0V4h4v16h-4Zm6 0v-7h4v7h-4Z" />
|
||||
<path
|
||||
v-else-if="name === 'content'"
|
||||
d="M4 3h16v18H4V3Zm3 4v2h10V7H7Zm0 4v2h10v-2H7Zm0 4v2h7v-2H7Z"
|
||||
/>
|
||||
<path v-else d="M12 2a7 7 0 0 1 7 7v4l2 3v1H3v-1l2-3V9a7 7 0 0 1 7-7Zm-3 17h6a3 3 0 0 1-6 0Z" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,5 +1,12 @@
|
||||
:root {
|
||||
--color-primary: #ff6b35;
|
||||
--primitive-orange-500: #ff6b35;
|
||||
--primitive-orange-600: #e95a28;
|
||||
--primitive-navy-900: #202c38;
|
||||
--primitive-navy-800: #293746;
|
||||
--primitive-gray-50: #f5f7f9;
|
||||
--primitive-gray-200: #e4e8ec;
|
||||
--primitive-white: #fff;
|
||||
--color-primary: var(--primitive-orange-500);
|
||||
--color-primary-hover: #e95a28;
|
||||
--color-primary-soft: #fff1eb;
|
||||
--color-text: #1f2329;
|
||||
@@ -8,10 +15,10 @@
|
||||
--color-success: #237a2b;
|
||||
--color-warning: #8a5a00;
|
||||
--color-info: #145ea8;
|
||||
--color-surface: #fff;
|
||||
--color-background: #f5f5f5;
|
||||
--color-border: #d9dce1;
|
||||
--color-sidebar: #263342;
|
||||
--color-surface: var(--primitive-white);
|
||||
--color-background: var(--primitive-gray-50);
|
||||
--color-border: var(--primitive-gray-200);
|
||||
--color-sidebar: var(--primitive-navy-900);
|
||||
--focus-ring: 0 0 0 3px rgb(255 107 53 / 28%);
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
@@ -211,6 +218,7 @@ input[aria-invalid='true'] {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.nav-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 10px 14px;
|
||||
@@ -219,6 +227,15 @@ input[aria-invalid='true'] {
|
||||
color: #e8edf2;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.admin-icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
flex: 0 0 auto;
|
||||
fill: currentColor;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.nav-item--active {
|
||||
color: #fff;
|
||||
@@ -246,6 +263,234 @@ input[aria-invalid='true'] {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.page-kicker {
|
||||
margin: 0 0 6px !important;
|
||||
color: var(--color-primary) !important;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dashboard-summary .summary-card {
|
||||
border: 0;
|
||||
border-top: 3px solid var(--color-primary);
|
||||
box-shadow: 0 5px 18px rgb(28 39 51 / 7%);
|
||||
}
|
||||
.dashboard-summary .summary-card:nth-child(2) {
|
||||
border-color: #378fe8;
|
||||
}
|
||||
.dashboard-summary .summary-card:nth-child(3) {
|
||||
border-color: #42af57;
|
||||
}
|
||||
.dashboard-summary .summary-card:nth-child(4) {
|
||||
border-color: #ffb13b;
|
||||
}
|
||||
.dashboard-charts {
|
||||
display: grid;
|
||||
margin-bottom: 20px;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.8fr);
|
||||
gap: 20px;
|
||||
}
|
||||
.chart-panel {
|
||||
min-height: 360px;
|
||||
}
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
.panel-heading h2 {
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
.panel-heading p,
|
||||
.panel-heading > span {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
height: 220px;
|
||||
padding: 18px 10px 0;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
#eef1f4 0,
|
||||
#eef1f4 1px,
|
||||
transparent 1px,
|
||||
transparent 55px
|
||||
);
|
||||
}
|
||||
.bar-column {
|
||||
display: flex;
|
||||
min-width: 36px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.bar-values {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.bar {
|
||||
display: block;
|
||||
width: min(18px, 35%);
|
||||
min-height: 5px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.bar--application {
|
||||
background: #4b98ee;
|
||||
}
|
||||
.bar--completed {
|
||||
background: #ff7848;
|
||||
}
|
||||
.bar-column small {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
margin-top: 18px;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.legend-dot {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-right: 7px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.legend-dot--blue {
|
||||
background: #4b98ee;
|
||||
}
|
||||
.legend-dot--orange {
|
||||
background: #ff7848;
|
||||
}
|
||||
.distribution-list {
|
||||
display: grid;
|
||||
gap: 19px;
|
||||
}
|
||||
.distribution-list > div > div {
|
||||
display: flex;
|
||||
margin-bottom: 7px;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
.distribution-list progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
.quick-panel {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.quick-actions button {
|
||||
display: flex;
|
||||
min-height: 76px;
|
||||
padding: 14px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
color: var(--color-text);
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
.quick-actions b {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
.quick-actions .blue {
|
||||
background: #378fe8;
|
||||
}
|
||||
.quick-actions .green {
|
||||
background: #42af57;
|
||||
}
|
||||
.quick-actions .gold {
|
||||
background: #e8a22c;
|
||||
}
|
||||
.muted-text {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.case-stepper {
|
||||
display: grid;
|
||||
margin-bottom: 20px;
|
||||
padding: 24px 32px;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.case-stepper div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.case-stepper div:not(:last-child)::after {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 14px;
|
||||
left: calc(50% + 20px);
|
||||
width: calc(100% - 40px);
|
||||
height: 2px;
|
||||
content: '';
|
||||
background: #dde2e6;
|
||||
}
|
||||
.case-stepper i {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border: 2px solid #cbd2d8;
|
||||
border-radius: 50%;
|
||||
color: #7c858d;
|
||||
background: #fff;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
.case-stepper .done {
|
||||
color: #268948;
|
||||
font-weight: 600;
|
||||
}
|
||||
.case-stepper .done i {
|
||||
border-color: #3daf5c;
|
||||
color: #fff;
|
||||
background: #3daf5c;
|
||||
}
|
||||
.case-stepper .done:not(:last-child)::after {
|
||||
background: #78ca8d;
|
||||
}
|
||||
.admin-main {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -742,6 +987,12 @@ button.summary-card:hover {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.dashboard-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.quick-actions {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -789,6 +1040,9 @@ button.summary-card:hover {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.quick-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { SERVICE_CATEGORY_LABELS, type StatisticsOverview } from '@lehuo/contracts';
|
||||
import { fetchStatistics } from '../api/statistics';
|
||||
|
||||
const emit = defineEmits<{ navigate: [view: string] }>();
|
||||
const data = ref<StatisticsOverview | null>(null);
|
||||
const loading = ref(true);
|
||||
const message = ref('');
|
||||
const maxMonthly = computed(() =>
|
||||
Math.max(1, ...(data.value?.monthly.map((item) => item.applications) ?? [1])),
|
||||
);
|
||||
const totalCategory = computed(() =>
|
||||
Math.max(1, data.value?.byCategory.reduce((sum, item) => sum + item.count, 0) ?? 1),
|
||||
);
|
||||
const palette = ['#378fe8', '#ff7041', '#ffb84a', '#57bf58', '#7a67d8'];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
data.value = await fetchStatistics();
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '数据加载失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="content-area dashboard-page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<p class="page-kicker">管理后台</p>
|
||||
<h1>工作概览</h1>
|
||||
<p>业务数据由系统实时汇总,帮助团队掌握服务进展。</p>
|
||||
</div>
|
||||
<button class="secondary-button compact-button" @click="load">刷新数据</button>
|
||||
</header>
|
||||
<p v-if="message" class="global-message" role="alert">{{ message }}</p>
|
||||
<section v-if="loading" class="summary-grid">
|
||||
<div v-for="n in 4" :key="n" class="summary-card table-skeleton"></div>
|
||||
</section>
|
||||
<template v-else-if="data">
|
||||
<section class="summary-grid dashboard-summary">
|
||||
<article class="summary-card">
|
||||
<span>服务对象总数</span
|
||||
><strong>{{ data.uniqueBeneficiaryCount.toLocaleString() }}</strong
|
||||
><small>统计区间内去重人数</small>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<span>服务工单完成</span><strong>{{ data.completedServiceCount.toLocaleString() }}</strong
|
||||
><small>已完成服务事实</small>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<span>志愿者人数</span><strong>{{ data.volunteerCount.toLocaleString() }}</strong
|
||||
><small>已登记志愿者</small>
|
||||
</article>
|
||||
<article class="summary-card">
|
||||
<span>服务人次</span><strong>{{ data.servicePersonTimes.toLocaleString() }}</strong
|
||||
><small>累计完成记录</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-charts">
|
||||
<article class="panel chart-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>服务工作趋势</h2>
|
||||
<p>申请量与完成服务量</p>
|
||||
</div>
|
||||
<span>{{ data.period.startDate }} — {{ data.period.endDate }}</span>
|
||||
</div>
|
||||
<div class="bar-chart" role="img" aria-label="按月服务工作趋势">
|
||||
<div v-for="item in data.monthly" :key="item.month" class="bar-column">
|
||||
<div class="bar-values">
|
||||
<i
|
||||
class="bar bar--application"
|
||||
:style="{ height: `${Math.max(5, (item.applications / maxMonthly) * 100)}%` }"
|
||||
:title="`申请 ${item.applications}`"
|
||||
></i
|
||||
><i
|
||||
class="bar bar--completed"
|
||||
:style="{
|
||||
height: `${Math.max(5, (item.completedServices / maxMonthly) * 100)}%`,
|
||||
}"
|
||||
:title="`完成 ${item.completedServices}`"
|
||||
></i>
|
||||
</div>
|
||||
<small>{{ item.month.slice(5) }}月</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<span><i class="legend-dot legend-dot--blue"></i>申请</span
|
||||
><span><i class="legend-dot legend-dot--orange"></i>完成服务</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="panel chart-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>服务类型分布</h2>
|
||||
<p>按申请服务类别统计</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="distribution-list">
|
||||
<div v-for="(item, index) in data.byCategory" :key="item.category">
|
||||
<div>
|
||||
<span
|
||||
><i
|
||||
class="legend-dot"
|
||||
:style="{ background: palette[index % palette.length] }"
|
||||
></i
|
||||
>{{ SERVICE_CATEGORY_LABELS[item.category] }}</span
|
||||
><strong>{{ item.count }}</strong>
|
||||
</div>
|
||||
<progress :value="item.count" :max="totalCategory"></progress>
|
||||
</div>
|
||||
<p v-if="!data.byCategory.length" class="muted-text">当前区间暂无分类数据</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel quick-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>快捷入口</h2>
|
||||
<p>进入高频业务工作台</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<button @click="emit('navigate', 'applications')">
|
||||
<b>申</b><span>新增服务跟进</span></button
|
||||
><button @click="emit('navigate', 'cases')">
|
||||
<b class="blue">单</b><span>处理服务工单</span></button
|
||||
><button @click="emit('navigate', 'volunteers')">
|
||||
<b class="green">愿</b><span>志愿者审核</span></button
|
||||
><button @click="emit('navigate', 'content')">
|
||||
<b class="gold">讯</b><span>发布公益内容</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
@@ -318,6 +318,25 @@ onMounted(() => load());
|
||||
仅可将以下信息用于已分配的公益服务;查看、指派和记录操作均会留痕。
|
||||
</div>
|
||||
<p v-if="message" class="global-message" role="status">{{ message }}</p>
|
||||
<section class="case-stepper" aria-label="工单处理进度">
|
||||
<div class="done"><i>1</i><span>已建单</span></div>
|
||||
<div
|
||||
:class="{
|
||||
done: ['ASSIGNED', 'IN_SERVICE', 'SERVICE_COMPLETED'].includes(selected.status),
|
||||
}"
|
||||
>
|
||||
<i>2</i><span>已指派</span>
|
||||
</div>
|
||||
<div :class="{ done: ['IN_SERVICE', 'SERVICE_COMPLETED'].includes(selected.status) }">
|
||||
<i>3</i><span>服务中</span>
|
||||
</div>
|
||||
<div :class="{ done: selected.status === 'SERVICE_COMPLETED' }">
|
||||
<i>4</i><span>服务完成</span>
|
||||
</div>
|
||||
<div :class="{ done: selected.status === 'SERVICE_COMPLETED' }">
|
||||
<i>5</i><span>进入回访</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="detail-grid">
|
||||
<article class="panel">
|
||||
<h2>服务计划</h2>
|
||||
|
||||
Reference in New Issue
Block a user