jiaxing.zhou

feat(frontend): 添加异常工单页面及路由配置

- 在路由中新增异常工单页面路径 /main/exception-workorder
- 在主布局菜单中添加异常工单入口
- 引入 Element Plus 的 ElCard 和 ElIcon 组件类型声明
- 重构异常工单页面 UI,替换原有 element 组件为原生 HTML 元素
- 新增表格、分页、搜索区域的自定义样式和交互逻辑
- 添加全选、跳转页码、页面大小切换等功能
- 优化状态和严重程度的展示样式及分类逻辑
......@@ -8,12 +8,14 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
......
......@@ -147,6 +147,7 @@ const menuItems = ref([
{ name: '产品管理', path: '/main/product', icon: '📦' },
{ name: '经销商管理', path: '/main/dealer', icon: '🏢' },
{ name: '返利管理', path: '/main/rebate', icon: '💰' },
{ name: '异常工单', path: '/main/exception-workorder', icon: '⚠️' },
{
name: '系统设置',
icon: '⚙️',
......
......@@ -153,6 +153,24 @@ const staticRoutes: RouteRecordRaw[] = [
}
},
{
path: 'exception-workorder',
name: 'ExceptionWorkorder',
component: () => import('@/views/exceptionWorkorder/index.vue'),
meta: {
title: '异常工单',
requiresAuth: true
}
},
{
path: 'test-menu',
name: 'TestMenu',
component: () => import('@/views/test-menu.vue'),
meta: {
title: '菜单测试',
requiresAuth: true
}
},
{
path: 'settings',
name: 'Settings',
component: () => import('@/views/settings/index.vue'),
......
......@@ -2,137 +2,73 @@
<div class="exception-workorder-container">
<!-- 搜索区域 -->
<div class="search-section">
<div class="search-form">
<div class="form-row">
<div class="form-item">
<div class="search-row">
<div class="search-item">
<label>工单编号:</label>
<el-input
<input
v-model="searchParams.workorderNo"
placeholder="请输入工单编号"
clearable
class="search-input"
placeholder="请输入工单编号"
/>
</div>
<div class="form-item">
<div class="search-item">
<label>关联订单号:</label>
<el-input
<input
v-model="searchParams.orderNo"
placeholder="请输入关联订单号"
clearable
class="search-input"
placeholder="请输入关联订单号"
/>
</div>
<div class="form-item">
<div class="search-item">
<label>经销商编码:</label>
<el-input
<input
v-model="searchParams.dealerCode"
placeholder="请输入经销商编码"
clearable
class="search-input"
placeholder="请输入经销商编码"
/>
</div>
<div class="form-item">
<div class="search-item">
<label>经销商名称:</label>
<el-input
<input
v-model="searchParams.dealerName"
placeholder="请输入经销商名称"
clearable
class="search-input"
placeholder="请输入经销商名称"
/>
</div>
</div>
<div class="form-row">
<div class="form-item">
<div class="search-row">
<div class="search-item">
<label>工单状态:</label>
<el-select
v-model="searchParams.workorderStatus"
placeholder="请选择工单状态"
clearable
class="search-select"
>
<el-option
v-for="(name, value) in WORKORDER_STATUS"
:key="value"
:label="name"
:value="Number(value)"
/>
</el-select>
<select v-model="searchParams.workorderStatus" class="search-select">
<option value="">所有</option>
<option v-for="(name, value) in WORKORDER_STATUS" :key="value" :value="Number(value)">{{ name }}</option>
</select>
</div>
<div class="form-item">
<div class="search-item">
<label>异常类型:</label>
<el-select
v-model="searchParams.exceptionType"
placeholder="请选择异常类型"
clearable
class="search-select"
>
<el-option
v-for="(name, value) in EXCEPTION_TYPE"
:key="value"
:label="name"
:value="Number(value)"
/>
</el-select>
<select v-model="searchParams.exceptionType" class="search-select">
<option value="">所有</option>
<option v-for="(name, value) in EXCEPTION_TYPE" :key="value" :value="Number(value)">{{ name }}</option>
</select>
</div>
<div class="form-item">
<div class="search-item">
<label>严重程度:</label>
<el-select
v-model="searchParams.severityLevel"
placeholder="请选择严重程度"
clearable
class="search-select"
>
<el-option
v-for="(name, value) in SEVERITY_LEVEL"
:key="value"
:label="name"
:value="Number(value)"
/>
</el-select>
<select v-model="searchParams.severityLevel" class="search-select">
<option value="">所有</option>
<option v-for="(name, value) in SEVERITY_LEVEL" :key="value" :value="Number(value)">{{ name }}</option>
</select>
</div>
<div class="form-item">
<div class="search-item">
<label>处理人:</label>
<el-input
<input
v-model="searchParams.handlerUser"
placeholder="请输入处理人"
clearable
class="search-input"
placeholder="请输入处理人"
/>
</div>
</div>
<div class="form-row">
<div class="form-item">
<label>开始日期:</label>
<el-date-picker
v-model="searchParams.startTime"
type="datetime"
placeholder="选择开始日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="search-date"
/>
</div>
<div class="form-item">
<label>结束日期:</label>
<el-date-picker
v-model="searchParams.endTime"
type="datetime"
placeholder="选择结束日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="search-date"
/>
</div>
<div class="form-item">
<el-button type="primary" @click="handleSearch" class="search-btn">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset" class="reset-btn">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
<div class="search-actions">
<button @click="handleSearch" class="search-btn">🔍 搜索</button>
<button @click="handleReset" class="reset-btn">🔄 重置</button>
</div>
</div>
</div>
......@@ -140,99 +76,136 @@
<!-- 操作按钮区域 -->
<div class="action-section">
<div class="action-buttons">
<el-button type="warning" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>
新增数据
</el-button>
<el-button type="success" @click="handleBatchProcess" class="batch-btn">
<el-icon><Operation /></el-icon>
批量处理
</el-button>
<el-button @click="handleExport" class="export-btn">
<el-icon><Download /></el-icon>
导出
</el-button>
<button @click="handleAdd" class="action-btn primary">✨ 新增</button>
<button @click="handleBatchProcess" class="action-btn success">🔧 批量处理</button>
<button @click="handleExport" class="action-btn secondary">📋 导出</button>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<el-table
:data="workorderList"
v-loading="loading"
@selection-change="handleSelectionChange"
class="data-table"
stripe
border
>
<el-table-column type="selection" width="55" />
<el-table-column prop="workorderId" label="工单ID" width="120" />
<el-table-column prop="workorderNo" label="工单编号" width="180" />
<el-table-column prop="orderNo" label="关联订单号" width="180" />
<el-table-column prop="dealerName" label="经销商名称" width="150" />
<el-table-column prop="workorderStatusName" label="工单状态" width="100">
<template #default="{ row }">
<el-tag :color="getWorkorderStatusColor(row.workorderStatus)">
{{ row.workorderStatusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="exceptionTypeName" label="异常类型" width="120" />
<el-table-column prop="severityLevelName" label="严重程度" width="100">
<template #default="{ row }">
<el-tag :color="getSeverityLevelColor(row.severityLevel)">
{{ row.severityLevelName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="日期" width="180" sortable>
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleEditStatus(row)"
class="action-btn"
>
编辑状态
</el-button>
<el-button
type="info"
size="small"
@click="handleViewLogs(row)"
class="action-btn"
<div class="table-header">
<div class="table-controls">
<button @click="handleTableSearch" class="control-btn">🔍</button>
<button @click="handleTableRefresh" class="control-btn">🔄</button>
<button @click="handleTableExport" class="control-btn">📋</button>
<button @click="handleTableViewToggle" class="control-btn">⊞</button>
</div>
</div>
<div class="table-container">
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">加载中...</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>
<input
type="checkbox"
class="select-all"
v-model="selectAll"
@change="handleSelectAll"
/>
</th>
<th>工单ID</th>
<th class="sortable">工单编号 ↕️</th>
<th>关联订单号</th>
<th>经销商名称</th>
<th>工单状态</th>
<th>异常类型</th>
<th>严重程度</th>
<th class="sortable">日期 ↕️</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="workorder in workorderList" :key="workorder.workorderId">
<td>
<input
type="checkbox"
class="row-select"
:value="workorder"
v-model="selectedWorkorders"
/>
</td>
<td>{{ workorder.workorderId }}</td>
<td>{{ workorder.workorderNo }}</td>
<td>{{ workorder.orderNo }}</td>
<td>{{ workorder.dealerName }}</td>
<td>
<span
class="status-badge"
:class="getWorkorderStatusClass(workorder.workorderStatus)"
>
处理日志
</el-button>
<el-button
type="success"
size="small"
@click="handleViewDetail(row)"
class="action-btn"
{{ workorder.workorderStatusName }}
</span>
</td>
<td>{{ workorder.exceptionTypeName }}</td>
<td>
<span
class="severity-badge"
:class="getSeverityLevelClass(workorder.severityLevel)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
{{ workorder.severityLevelName }}
</span>
</td>
<td>{{ formatDateTime(workorder.createTime) }}</td>
<td>
<div class="table-actions">
<button @click="handleEditStatus(workorder)" class="table-btn edit">编辑状态</button>
<button @click="handleViewLogs(workorder)" class="table-btn info">处理日志</button>
<button @click="handleViewDetail(workorder)" class="table-btn view">详情</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
<div class="pagination-info">
<span>共 {{ pagination.total }} 条</span>
<select v-model="pagination.pageSize" @change="handlePageSizeChange" class="page-size-select">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
<div class="pagination-controls">
<button
@click="handleCurrentChange(1)"
:disabled="pagination.pageNum === 1"
class="page-btn"
>首页</button>
<button
@click="handleCurrentChange(pagination.pageNum - 1)"
:disabled="pagination.pageNum === 1"
class="page-btn"
>上一页</button>
<span class="page-info">{{ pagination.pageNum }} / {{ Math.ceil(pagination.total / pagination.pageSize) }}</span>
<button
@click="handleCurrentChange(pagination.pageNum + 1)"
:disabled="pagination.pageNum >= Math.ceil(pagination.total / pagination.pageSize)"
class="page-btn"
>下一页</button>
<button
@click="handleCurrentChange(Math.ceil(pagination.total / pagination.pageSize))"
:disabled="pagination.pageNum >= Math.ceil(pagination.total / pagination.pageSize)"
class="page-btn"
>末页</button>
<span>跳至</span>
<input
v-model="jumpPage"
@keyup.enter="handleJumpPage"
class="jump-input"
placeholder="页码"
/>
<span>页</span>
</div>
</div>
<!-- 新增/编辑对话框 -->
......@@ -513,6 +486,8 @@ const workorderList = ref<ExceptionWorkorderInfo[]>([])
const selectedWorkorders = ref<ExceptionWorkorderInfo[]>([])
const workorderDetail = ref<ExceptionWorkorderInfo | null>(null)
const workorderLogs = ref<any[]>([])
const selectAll = ref(false)
const jumpPage = ref<number | string>('')
// 分页数据
const pagination = reactive({
......@@ -588,6 +563,27 @@ const statusFormRules = {
]
}
// 获取工单状态类型
const getWorkorderStatusType = (status: number) => {
switch (status) {
case 1: return 'warning' // 待处理
case 2: return 'primary' // 处理中
case 3: return 'success' // 已解决
case 4: return 'info' // 已关闭
default: return 'info'
}
}
// 获取严重程度类型
const getSeverityLevelType = (level: number) => {
switch (level) {
case 1: return 'danger' // 高
case 2: return 'warning' // 中
case 3: return 'success' // 低
default: return 'info'
}
}
// 获取异常工单列表
const fetchWorkorders = async () => {
try {
......@@ -603,6 +599,10 @@ const fetchWorkorders = async () => {
if (response && response.records) {
workorderList.value = response.records
pagination.total = response.total
} else if (response && Array.isArray(response)) {
// 如果直接返回数组
workorderList.value = response
pagination.total = response.length
} else {
workorderList.value = []
pagination.total = 0
......@@ -797,9 +797,70 @@ const handleDownload = (url: string) => {
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss')
}
// 获取工单状态样式类
const getWorkorderStatusClass = (status: number) => {
switch (status) {
case 1: return 'pending' // 待处理
case 2: return 'processing' // 处理中
case 3: return 'resolved' // 已解决
case 4: return 'closed' // 已关闭
default: return 'unknown'
}
}
// 获取严重程度样式类
const getSeverityLevelClass = (level: number) => {
switch (level) {
case 1: return 'high' // 高
case 2: return 'medium' // 中
case 3: return 'low' // 低
default: return 'unknown'
}
}
// 表格操作方法
const handleSelectAll = () => {
if (selectAll.value) {
selectedWorkorders.value = [...workorderList.value]
} else {
selectedWorkorders.value = []
}
}
const handleTableSearch = () => {
handleSearch()
}
const handleTableRefresh = () => {
fetchWorkorders()
}
const handleTableExport = () => {
handleExport()
}
const handleTableViewToggle = () => {
// 表格视图切换逻辑
console.log('表格视图切换')
}
const handleJumpPage = () => {
const page = Number(jumpPage.value)
if (page && page > 0 && page <= Math.ceil(pagination.total / pagination.pageSize)) {
handleCurrentChange(page)
jumpPage.value = ''
}
}
const handlePageSizeChange = () => {
pagination.pageNum = 1
fetchWorkorders()
}
// 组件挂载
onMounted(() => {
fetchWorkorders()
......@@ -808,93 +869,400 @@ onMounted(() => {
<style scoped>
.exception-workorder-container {
padding: 20px;
padding: 0px;
background: #f5f5f5;
min-height: 100vh;
}
/* 搜索区域样式 */
.search-section {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search-form {
display: flex;
flex-direction: column;
gap: 15px;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
}
.form-row {
.search-row {
display: flex;
gap: 20px;
gap: 25px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 12px;
}
.form-item {
.search-row:last-child {
margin-bottom: 0;
}
.search-item {
display: flex;
align-items: center;
gap: 8px;
min-width: 200px;
flex: 0 0 auto;
}
.form-item label {
font-weight: 500;
color: #333;
.search-item label {
color: #666;
font-size: 14px;
width: 90px;
white-space: nowrap;
min-width: 80px;
text-align: right;
}
.search-input {
width: 180px;
height: 32px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
}
.search-input,
.search-select,
.search-date {
width: 200px;
.search-select {
width: 160px;
height: 32px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
background: white;
}
.search-actions {
margin-left: auto;
display: flex;
gap: 10px;
}
.search-btn, .reset-btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
height: 32px;
}
.search-btn {
background: #007bff;
color: white;
}
.search-btn,
.reset-btn {
margin-left: 10px;
background: #6c757d;
color: white;
}
/* 操作按钮区域样式 */
.action-section {
background: white;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 12px 20px 12px 20px;
border-bottom: 1px solid #e0e0e0;
}
.action-buttons {
.action-section .action-buttons {
display: flex;
gap: 10px;
justify-content: flex-start;
align-items: center;
margin: 0;
padding: 0;
}
.action-btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
height: 32px;
font-weight: 500;
}
.action-btn.primary {
background: #007bff;
color: white;
}
.action-btn.success {
background: #28a745;
color: white;
}
.action-btn.secondary {
background: #6c757d;
color: white;
}
/* 表格区域样式 */
.table-section {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
margin-bottom: 0;
}
.table-header {
padding: 10px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: flex-end;
}
.table-controls {
display: flex;
gap: 5px;
}
.control-btn {
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.table-container {
position: relative;
overflow-x: auto;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
padding: 20px;
font-size: 16px;
color: #666;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.data-table th {
background: #f8f9fa;
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.data-table td {
border: 1px solid #ddd;
padding: 12px 8px;
vertical-align: middle;
}
.data-table tbody tr:nth-child(even) {
background: #f8f9fa;
}
.data-table tbody tr:hover {
background: #e9ecef;
}
.sortable {
cursor: pointer;
user-select: none;
}
.select-all, .row-select {
margin: 0;
}
.status-badge, .severity-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.status-badge.pending {
background: #fff3cd;
color: #856404;
}
.status-badge.processing {
background: #d1ecf1;
color: #0c5460;
}
.status-badge.resolved {
background: #d4edda;
color: #155724;
}
.status-badge.closed {
background: #f8d7da;
color: #721c24;
}
.severity-badge.high {
background: #f8d7da;
color: #721c24;
}
.severity-badge.medium {
background: #fff3cd;
color: #856404;
}
.severity-badge.low {
background: #d4edda;
color: #155724;
}
.table-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.table-btn {
padding: 2px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.table-btn.edit {
background: #007bff;
color: white;
}
.table-btn.info {
background: #6c757d;
color: white;
}
.table-btn.view {
background: #28a745;
color: white;
}
/* 状态标签样式 */
.status-tag {
font-weight: 500;
border-radius: 4px;
}
.severity-tag {
font-weight: 500;
border-radius: 4px;
}
/* 表格内操作按钮容器 */
.table-actions {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 0 4px;
}
/* 操作按钮样式 */
.action-btn {
margin-right: 5px;
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
font-weight: 500;
min-width: 60px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 分页区域样式 */
.pagination-section {
background: white;
padding: 12px 20px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
}
.pagination-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: #666;
}
.page-size-select {
height: 28px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.page-btn {
padding: 4px 12px;
border: 1px solid #ddd;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.page-btn:disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
.page-btn:not(:disabled):hover {
background: #007bff;
color: white;
border-color: #007bff;
}
.page-info {
margin: 0 8px;
color: #666;
}
.jump-input {
width: 50px;
height: 28px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 8px;
text-align: center;
font-size: 14px;
}
.add-dialog,
......