171 lines
6.3 KiB
Vue
171 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import {
|
||
CircleCheck,
|
||
Finished,
|
||
House,
|
||
Warning,
|
||
} from '@element-plus/icons-vue'
|
||
|
||
import AppShell from '@/components/AppShell.vue'
|
||
import { getUser } from '@/lib/auth'
|
||
import api from '@/lib/api'
|
||
import { ACTIVE_STATUSES, CLOSED_STATUSES } from '@/lib/status'
|
||
import type { OrderSummary } from '@/types'
|
||
|
||
const router = useRouter()
|
||
const user = computed(() => getUser())
|
||
const orders = ref<OrderSummary[]>([])
|
||
const loadingOrders = ref(false)
|
||
const totalOrders = computed(() => orders.value.length)
|
||
const isEmpty = computed(() => totalOrders.value === 0 && !loadingOrders.value)
|
||
const activeOrders = computed(() => orders.value.filter((order) => ACTIVE_STATUSES.has(order.status)).length)
|
||
const closedOrders = computed(() => orders.value.filter((order) => CLOSED_STATUSES.has(order.status)).length)
|
||
const currentOrders = computed(() => orders.value.filter((order) => ACTIVE_STATUSES.has(order.status)).slice(0, 3))
|
||
const recentOrders = computed(() => orders.value.slice(0, 4))
|
||
const serviceStats = computed(() => [
|
||
{ label: '累计工单', value: totalOrders.value, icon: Finished },
|
||
{ label: '处理中', value: activeOrders.value, icon: Warning },
|
||
{ label: '已办结', value: closedOrders.value, icon: CircleCheck },
|
||
])
|
||
|
||
async function loadOrders() {
|
||
loadingOrders.value = true
|
||
try {
|
||
const response = await api.get<OrderSummary[]>('/api/student/orders')
|
||
orders.value = response.data
|
||
} catch {
|
||
orders.value = []
|
||
} finally {
|
||
loadingOrders.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadOrders()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<AppShell accent="student">
|
||
<h1 class="page-title">后勤报修服务</h1>
|
||
<p class="page-subtitle">提交宿舍设施报修,查看维修进度与处理结果。</p>
|
||
<div class="portal-home">
|
||
<section class="portal-overview-strip">
|
||
<el-card class="portal-profile-card" shadow="never">
|
||
<div class="portal-profile-card__main">
|
||
<el-avatar :size="54">
|
||
<el-icon><House /></el-icon>
|
||
</el-avatar>
|
||
<div class="portal-profile-card__text">
|
||
<span>西海岸校区后勤报修</span>
|
||
<h1>{{ user?.display_name ?? '同学' }},需要报修从这里开始</h1>
|
||
<p>宿舍设施故障先提交报修;已提交的问题在"我的工单"查看进度。</p>
|
||
<div class="portal-profile-card__actions">
|
||
<button class="button" type="button" @click="router.push('/student/report')">提交报修</button>
|
||
<button class="text-button" type="button" @click="router.push('/student/orders')">查看我的工单</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</section>
|
||
|
||
<section v-if="isEmpty" class="welcome-card card">
|
||
<div class="welcome-card__content">
|
||
<h2>还没有报修记录</h2>
|
||
<p>遇到宿舍设施问题?从提交第一份工单开始。</p>
|
||
<div class="welcome-card__actions">
|
||
<button class="button" type="button" @click="router.push('/student/report')">提交报修</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<template v-else>
|
||
|
||
<section class="portal-stats-row" aria-label="报修统计">
|
||
<el-card
|
||
v-for="stat in serviceStats"
|
||
:key="stat.label"
|
||
class="portal-stat-card"
|
||
shadow="never"
|
||
>
|
||
<div class="portal-stat-card__icon">
|
||
<el-icon><component :is="stat.icon" /></el-icon>
|
||
</div>
|
||
<span>{{ stat.label }}</span>
|
||
<strong>{{ stat.value }}</strong>
|
||
</el-card>
|
||
</section>
|
||
|
||
<section class="portal-workspace">
|
||
<el-card class="portal-orders-card" shadow="never">
|
||
<template #header>
|
||
<div class="portal-card-header">
|
||
<div>
|
||
<span>当前进度</span>
|
||
<h2>需要关注的报修</h2>
|
||
</div>
|
||
<el-button type="primary" plain @click="router.push('/student/orders')">全部工单</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<el-skeleton v-if="loadingOrders" :rows="4" animated />
|
||
<el-empty v-else-if="currentOrders.length === 0" description="当前没有未办结工单" />
|
||
<el-table
|
||
v-else
|
||
class="portal-order-table"
|
||
:data="currentOrders"
|
||
:show-header="false"
|
||
@row-click="(order: OrderSummary) => router.push(`/student/orders/${order.id}`)"
|
||
>
|
||
<el-table-column min-width="220">
|
||
<template #default="{ row }">
|
||
<div class="portal-order-main">
|
||
<strong>{{ row.category }}</strong>
|
||
<span>{{ row.building }} {{ row.room }} · {{ row.order_no }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="128">
|
||
<template #default="{ row }">
|
||
<el-tag effect="light" type="primary">{{ row.status }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column min-width="220">
|
||
<template #default="{ row }">
|
||
<span class="portal-muted">{{ row.assignee_name || '等待分配' }} / {{ row.expected_repair_time || '时间待定' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-card class="portal-recent-card" shadow="never">
|
||
<template #header>
|
||
<div class="portal-card-header">
|
||
<div>
|
||
<span>最近记录</span>
|
||
<h2>报修记录</h2>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<el-empty v-if="recentOrders.length === 0" description="暂无报修记录" />
|
||
<div v-else class="portal-recent-list">
|
||
<button
|
||
v-for="order in recentOrders"
|
||
:key="order.id"
|
||
type="button"
|
||
@click="router.push(`/student/orders/${order.id}`)"
|
||
>
|
||
<span>{{ order.category }}</span>
|
||
<el-tag size="small" type="info">{{ order.status }}</el-tag>
|
||
</button>
|
||
</div>
|
||
</el-card>
|
||
</section>
|
||
|
||
</template>
|
||
</div>
|
||
</AppShell>
|
||
</template>
|