init
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user