458 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="space-y-6">
<!-- 页面头部 -->
<section class="page-header">
<div>
<p class="page-kicker">Disposal</p>
<h2 class="page-title">报废流程</h2>
</div>
<div class="page-meta-grid">
<div class="page-meta">
<p class="page-meta__label">待审批</p>
<p class="page-meta__value">{{ pageSummary.pending }}</p>
</div>
<div class="page-meta">
<p class="page-meta__label">已审批</p>
<p class="page-meta__value">{{ pageSummary.approved }}</p>
</div>
<div class="page-meta">
<p class="page-meta__label">已完成</p>
<p class="page-meta__value">{{ pageSummary.completed }}</p>
</div>
<div class="page-meta">
<p class="page-meta__label">已驳回</p>
<p class="page-meta__value">{{ pageSummary.rejected }}</p>
</div>
</div>
</section>
<section class="card !p-3">
<div class="overflow-x-auto">
<div class="segmented-toggle min-w-max" style="padding: 2px;">
<button
v-for="scope in availableScopes"
:key="scope.value"
type="button"
class="segmented-toggle__item"
style="min-height: 32px; padding: 0 12px; font-size: 12px;"
:class="{ 'is-active': activeScope === scope.value }"
@click="changeScope(scope.value)"
>
{{ scope.label }}
</button>
</div>
</div>
</section>
<!-- 筛选区域 -->
<section class="card">
<div class="filter-bar">
<div class="filter-bar__inputs">
<div class="field-stack">
<label class="field-label">报废状态</label>
<select v-model="filters.status" class="select" @change="handleQuery">
<option value="">全部状态</option>
<option value="pending">待审批</option>
<option value="approved">已审批</option>
<option value="rejected">已驳回</option>
<option value="completed">已完成</option>
</select>
</div>
<div class="field-stack">
<label class="field-label">搜索记录</label>
<input
v-model.trim="filters.keyword"
type="text"
class="input"
placeholder="输入资产编码、名称、报废原因或处置方式"
@input="handleSearch"
@keyup.enter="handleQuery"
/>
</div>
</div>
<div class="filter-bar__actions">
<button class="btn-secondary text-xs sm:text-sm" :disabled="loading" @click="resetFilters">重置</button>
<button class="btn-primary text-xs sm:text-sm" :disabled="loading" @click="handleQuery">
{{ loading ? '查询中...' : '查询' }}
</button>
</div>
</div>
</section>
<!-- 数据列表 -->
<section class="card">
<div class="list-header">
<h3 class="section-title">报废记录</h3>
<button class="btn-secondary text-xs sm:text-sm" :disabled="loading" @click="loadList">刷新</button>
</div>
<!-- 移动端卡片 -->
<div class="mobile-card-list md:hidden">
<article v-for="item in list" :key="`mobile-${item.id}`" class="mobile-card">
<div class="mobile-card__row">
<div>
<p class="text-sm font-semibold text-text-primary">{{ item.equipment?.equipment_name || '-' }}</p>
<p class="mt-1 text-xs font-mono text-text-tertiary">{{ item.equipment_code }}</p>
</div>
<span class="badge" :class="getStatusBadgeClass(item.status)">
{{ getStatusText(item.status) }}
</span>
</div>
<div class="mobile-card__meta">
<div>
<p class="mobile-card__label">申请人</p>
<p class="mobile-card__value">{{ item.applier?.name || '-' }}</p>
</div>
<div>
<p class="mobile-card__label">申请时间</p>
<p class="mobile-card__value">{{ formatDate(item.applied_at) }}</p>
</div>
<div>
<p class="mobile-card__label">处置方式</p>
<p class="mobile-card__value">{{ item.disposal_method || '未填写' }}</p>
</div>
<div>
<p class="mobile-card__label">残值估价</p>
<p class="mobile-card__value">{{ formatCurrency(item.estimated_value) }}</p>
</div>
</div>
<div class="mt-4 rounded-lg border border-border bg-bg-elevated px-4 py-3">
<p class="mobile-card__label">报废原因</p>
<p class="mobile-card__value">{{ item.disposal_reason || '未填写' }}</p>
</div>
<div v-if="item.notes" class="mt-3 rounded-lg border border-border bg-bg-elevated px-4 py-3">
<p class="mobile-card__label">备注</p>
<p class="mobile-card__value">{{ item.notes }}</p>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button class="btn-secondary text-xs sm:text-sm" @click="openDetail(item)">查看详情</button>
<button
v-if="item.status === 'pending' && canApprove"
class="btn-primary text-xs sm:text-sm"
@click="handleApprove(item, true)"
>
通过
</button>
<button
v-if="item.status === 'pending' && canApprove"
class="btn-secondary text-xs sm:text-sm !text-status-error"
@click="handleApprove(item, false)"
>
驳回
</button>
<button
v-if="item.status === 'approved' && canComplete"
class="btn-secondary text-xs sm:text-sm"
@click="handleComplete(item)"
>
完成
</button>
</div>
</article>
<p v-if="!loading && list.length === 0" class="text-center text-sm text-text-tertiary py-8">
当前筛选条件下暂无报废记录。
</p>
</div>
<!-- 桌面端表格 -->
<div class="hidden md:block">
<div class="table-container">
<table class="data-table min-w-[1080px]">
<thead>
<tr>
<th>资产编码</th>
<th>资产名称</th>
<th>报废原因</th>
<th>处置方式</th>
<th class="text-right">残值估价</th>
<th>状态</th>
<th>申请人</th>
<th>申请时间</th>
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.id">
<td class="font-mono text-sm text-text-secondary">{{ item.equipment_code }}</td>
<td>
<p class="font-medium text-text-primary">{{ item.equipment?.equipment_name || '-' }}</p>
<p class="text-xs text-text-tertiary">{{ item.equipment?.equipment_type || '-' }}</p>
</td>
<td class="max-w-[260px]">
<p class="truncate text-sm text-text-secondary">{{ item.disposal_reason }}</p>
</td>
<td class="text-sm text-text-secondary">{{ item.disposal_method || '-' }}</td>
<td class="text-right text-sm text-text-primary">{{ formatCurrency(item.estimated_value) }}</td>
<td>
<span class="badge" :class="getStatusBadgeClass(item.status)">
{{ getStatusText(item.status) }}
</span>
</td>
<td class="text-sm text-text-secondary">{{ item.applier?.name || '-' }}</td>
<td class="text-sm text-text-secondary">{{ formatDate(item.applied_at) }}</td>
<td class="text-right">
<div class="table-action-group justify-end">
<button class="table-action" @click="openDetail(item)">详情</button>
<button
v-if="item.status === 'pending' && canApprove"
class="table-action table-action--primary"
@click="handleApprove(item, true)"
>
通过
</button>
<button
v-if="item.status === 'pending' && canApprove"
class="table-action table-action--danger"
@click="handleApprove(item, false)"
>
驳回
</button>
<button
v-if="item.status === 'approved' && canComplete"
class="table-action table-action--warning"
@click="handleComplete(item)"
>
完成
</button>
<span v-if="item.status === 'completed'" class="text-sm text-text-tertiary">已完成</span>
<span v-if="item.status === 'rejected'" class="text-sm text-text-tertiary">已驳回</span>
</div>
</td>
</tr>
<tr v-if="!loading && list.length === 0">
<td colspan="9" class="py-8 text-center text-text-tertiary">当前筛选条件下暂无报废记录。</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div class="pagination-bar">
<p class="text-sm text-text-secondary">第 {{ page }} / {{ totalPages }} 页</p>
<div class="flex items-center gap-2">
<button class="btn-secondary text-sm" :disabled="page <= 1 || loading" @click="handlePrevPage">上一页</button>
<button class="btn-secondary text-sm" :disabled="page >= totalPages || loading" @click="handleNextPage">下一页</button>
</div>
</div>
</section>
<!-- 完成处置弹窗 -->
<div v-if="showCompleteModal" class="sheet-backdrop">
<div class="sheet-panel max-w-2xl">
<h3 class="text-xl font-semibold text-text-primary mb-4">完成处置</h3>
<p class="text-sm text-text-secondary mb-4">
{{ currentItem?.equipment?.equipment_name || currentItem?.equipment_code || '当前资产' }} 已通过审批,请补充最终处置结果。
</p>
<div class="field-grid">
<div class="field-stack">
<label class="field-label">实际处置价值</label>
<input v-model.number="completeForm.actual_value" type="number" min="0" step="0.01" class="input" />
</div>
<div class="panel">
<p class="text-xs text-text-tertiary">残值估价</p>
<p class="mt-1 text-base font-medium text-text-primary">{{ formatCurrency(currentItem?.estimated_value || 0) }}</p>
</div>
</div>
<div class="mt-4 field-stack">
<label class="field-label">完成说明 *</label>
<textarea
v-model.trim="completeForm.completion_notes"
class="input"
rows="4"
placeholder="例如:已转交回收商,回收凭证已登记"
></textarea>
</div>
<div class="flex justify-end gap-3 mt-6">
<button class="btn-secondary" @click="showCompleteModal = false">取消</button>
<button class="btn-primary" :disabled="submittingComplete || !completeForm.completion_notes" @click="submitComplete">
{{ submittingComplete ? '提交中...' : '确认完成' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getDisposalList, approveDisposal, completeDisposal, type DisposalRecord } from '@/api/disposal'
import { useAuthStore } from '@/stores/auth'
import { PERMISSIONS } from '@/auth/permissions'
type DisposalScope = 'pending' | 'mine' | 'processed' | 'all'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const canApprove = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPROVE))
const canComplete = computed(() => authStore.can(PERMISSIONS.DISPOSALS_COMPLETE))
const canApply = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPLY))
const availableScopes = computed<Array<{ value: DisposalScope; label: string }>>(() => {
const scopes: Array<{ value: DisposalScope; label: string }> = []
if (canApprove.value) scopes.push({ value: 'pending', label: '待我审批' })
if (canApply.value) scopes.push({ value: 'mine', label: '我发起的' })
if (canApprove.value || canComplete.value) scopes.push({ value: 'processed', label: '已处理' })
if (authStore.can(PERMISSIONS.SYSTEM_SETTINGS)) scopes.push({ value: 'all', label: '全部' })
return scopes
})
const list = ref<DisposalRecord[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const filters = ref({ status: '', keyword: '' })
const showCompleteModal = ref(false)
const currentItem = ref<DisposalRecord | null>(null)
const completeForm = ref({ actual_value: 0, completion_notes: '' })
const submittingComplete = ref(false)
const activeScope = ref<DisposalScope>('pending')
let searchTimer: ReturnType<typeof setTimeout> | null = null
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const pageSummary = computed(() => ({
pending: list.value.filter((item) => item.status === 'pending').length,
approved: list.value.filter((item) => item.status === 'approved').length,
completed: list.value.filter((item) => item.status === 'completed').length,
rejected: list.value.filter((item) => item.status === 'rejected').length
}))
const loadList = async () => {
loading.value = true
try {
const res = await getDisposalList({ page: page.value, pageSize: pageSize.value, scope: activeScope.value, ...filters.value })
list.value = res.data?.list || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const handleSearch = () => {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
page.value = 1
loadList()
}, 300)
}
const handleQuery = () => {
page.value = 1
loadList()
}
const resetFilters = () => {
filters.value.status = ''
filters.value.keyword = ''
handleQuery()
}
const changeScope = (scope: DisposalScope) => {
activeScope.value = scope
page.value = 1
router.replace({ query: { ...route.query, scope } })
loadList()
}
const openDetail = (item: DisposalRecord) => router.push(`/workflows/disposals/${item.id}`)
const handleApprove = async (item: DisposalRecord, approved: boolean) => {
const msg = approved ? '确定通过此报废申请吗?' : '确定驳回此报废申请吗?'
if (!confirm(msg)) return
try {
const rejectReason = approved ? undefined : window.prompt('请填写驳回原因。', '')?.trim() || undefined
if (!approved && !rejectReason) {
alert('驳回原因不能为空')
return
}
await approveDisposal(item.id, { approved, reject_reason: rejectReason })
await loadList()
} catch (error: any) {
alert(error?.message || '申请状态已变化,请刷新后重试')
await loadList()
}
}
const handleComplete = (item: DisposalRecord) => {
currentItem.value = item
completeForm.value = { actual_value: Number(item.estimated_value || 0), completion_notes: item.completion_notes || '' }
showCompleteModal.value = true
}
const submitComplete = async () => {
if (!currentItem.value || !completeForm.value.completion_notes.trim()) return
try {
submittingComplete.value = true
await completeDisposal(currentItem.value.id, completeForm.value)
showCompleteModal.value = false
await loadList()
} catch (error: any) {
alert(error?.message || '申请状态已变化,请刷新后重试')
await loadList()
} finally {
submittingComplete.value = false
}
}
const handlePrevPage = async () => {
if (page.value <= 1) return
page.value -= 1
await loadList()
}
const handleNextPage = async () => {
if (page.value >= totalPages.value) return
page.value += 1
await loadList()
}
const getStatusBadgeClass = (status: string) => {
const map: Record<string, string> = {
pending: 'badge-warning',
approved: 'badge-neutral',
rejected: 'badge-error',
completed: 'badge-success'
}
return map[status] || ''
}
const getStatusText = (status: string) => {
const map: Record<string, string> = {
pending: '待审批',
approved: '已审批',
rejected: '已驳回',
completed: '已完成'
}
return map[status] || status
}
const formatDate = (date: string) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
const formatCurrency = (value: number) => `¥${Number(value || 0).toFixed(2)}`
onMounted(() => {
const requestedScope = route.query.scope as DisposalScope | undefined
const allowedScopes = availableScopes.value.map((scope) => scope.value)
activeScope.value = requestedScope && allowedScopes.includes(requestedScope)
? requestedScope
: allowedScopes[0] || 'mine'
loadList()
})
</script>