小程序工单设备代码传入

This commit is contained in:
2025-12-31 12:06:21 +08:00
parent 1c207c2439
commit 1ef1b32f5f
5 changed files with 193 additions and 125 deletions

View File

@@ -1,5 +1,10 @@
// 全局EL分页组件样式
// ==================== 品牌色变量 ====================
$brand-color: #0055AA;
$brand-color-light: #EBF5FF;
$brand-color-hover: #004488;
.content-header {
display: flex;
align-items: center;
@@ -190,11 +195,32 @@
align-items: center;
gap: 10px;
}
// 分页样式
.table-pagination {
margin-top: 12px;
padding: 16px 20px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #f1f5f9;
background: #fff;
}
// 全局分页样式(不需要 :deep因为这是全局样式
.el-pagination {
.el-pager {
li {
border-radius: 6px;
&.is-active {
background: $brand-color;
color: #fff;
}
}
}
.btn-prev,
.btn-next {
border-radius: 6px;
}
}
.file-name-cell {

View File

@@ -1,5 +1,5 @@
<template>
<AdminLayout title="工单日志" info="查看工单操作记录">
<AdminLayout title="工单日志" info="查看工单流程处理记录">
<template #action>
<el-button type="primary" @click="exportLogs">
<el-icon><Download /></el-icon>
@@ -11,46 +11,48 @@
<!-- 筛选区域 -->
<el-card class="filter-card">
<div class="ticket-filters">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 280px;" @change="handleSearch" />
<div class="filter-right">
<el-select v-model="operationFilter" placeholder="操作类型" clearable style="width: 140px;">
<el-option label="创建" value="create" />
<el-option label="更新" value="update" />
<el-option label="指派" value="assign" />
<el-option label="完成" value="complete" />
<el-option label="关闭" value="close" />
<el-select v-model="filter.action" placeholder="操作类型" clearable style="width: 140px;" @change="handleSearch">
<el-option v-for="item in actionOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="operatorFilter" placeholder="操作人" clearable style="width: 120px;">
<el-option label="王五" value="wangwu" />
<el-option label="赵六" value="zhaoliu" />
<el-option label="孙七" value="sunqi" />
</el-select>
<el-input v-model="searchKeyword" placeholder="搜索工单号/内容" style="width: 200px;" :prefix-icon="Search" clearable />
<el-input v-model="filter.workcaseId" placeholder="搜索工单ID" style="width: 200px;" :prefix-icon="Search" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
</div>
</el-card>
<!-- 日志列表 -->
<el-card>
<el-table :data="filteredLogs" style="width: 100%">
<el-table-column prop="logId" label="日志ID" width="120">
<el-table :data="processLogs" style="width: 100%" v-loading="loading">
<el-table-column prop="processId" label="流程ID" width="180">
<template #default="{ row }">
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
<span style="color: #409eff; font-weight: 500;">{{ row.processId }}</span>
</template>
</el-table-column>
<el-table-column prop="ticketNo" label="工单" width="120" />
<el-table-column prop="operation" label="操作类型" width="100">
<el-table-column prop="workcaseId" label="工单ID" width="180" />
<el-table-column prop="action" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="getOperationType(row.operation)" size="small">
{{ row.operationName }}
<el-tag :type="getActionTagType(row.action)" size="small">
{{ getActionLabel(row.action) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作" width="100" />
<el-table-column prop="content" label="操作内容" min-width="200" />
<el-table-column prop="operationTime" label="操作时间" width="160" />
<el-table-column prop="ipAddress" label="IP地址" width="130" />
<el-table-column prop="message" label="操作内容" min-width="200" show-overflow-tooltip />
<el-table-column prop="processor" label="处理人" width="120">
<template #default="{ row }">
{{ row.processor || '-' }}
</template>
</el-table-column>
<el-table-column prop="files" label="附件" width="80">
<template #default="{ row }">
<el-tag v-if="row.files?.length" size="small" type="info">{{ row.files.length }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="creator" label="操作人" width="120" />
<el-table-column prop="createTime" label="操作时间" width="170" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
@@ -59,99 +61,154 @@
</el-table>
<div class="table-pagination">
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="handleSearch"
@current-change="handleSearch"
/>
</div>
</el-card>
</div>
<!-- 日志详情弹窗 -->
<el-dialog v-model="showDetailDialog" title="日志详情" width="600px">
<el-form v-if="selectedLog" label-width="100px">
<el-form-item label="日志ID">
<span>{{ selectedLog.logId }}</span>
</el-form-item>
<el-form-item label="工单号">
<span>{{ selectedLog.ticketNo }}</span>
</el-form-item>
<el-form-item label="操作类型">
<el-tag :type="getOperationType(selectedLog.operation)">{{ selectedLog.operationName }}</el-tag>
</el-form-item>
<el-form-item label="操作人">
<span>{{ selectedLog.operator }}</span>
</el-form-item>
<el-form-item label="操作内容">
<span>{{ selectedLog.content }}</span>
</el-form-item>
<el-form-item label="操作时间">
<span>{{ selectedLog.operationTime }}</span>
</el-form-item>
<el-form-item label="IP地址">
<span>{{ selectedLog.ipAddress }}</span>
</el-form-item>
</el-form>
<el-dialog v-model="showDetailDialog" title="流程详情" width="600px">
<el-descriptions v-if="selectedLog" :column="1" border>
<el-descriptions-item label="流程ID">{{ selectedLog.processId }}</el-descriptions-item>
<el-descriptions-item label="工单ID">{{ selectedLog.workcaseId }}</el-descriptions-item>
<el-descriptions-item label="操作类型">
<el-tag :type="getActionTagType(selectedLog.action)">{{ getActionLabel(selectedLog.action) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="操作内容">{{ selectedLog.message || '-' }}</el-descriptions-item>
<el-descriptions-item label="处理人">{{ selectedLog.processor || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ selectedLog.creator }}</el-descriptions-item>
<el-descriptions-item label="操作时间">{{ selectedLog.createTime }}</el-descriptions-item>
<el-descriptions-item v-if="selectedLog.files?.length" label="附件">
<div class="file-list">
<el-tag v-for="(file, index) in selectedLog.files" :key="index" size="small" style="margin-right: 8px; margin-bottom: 4px;">
{{ file }}
</el-tag>
</div>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import AdminLayout from '@/views/admin/AdminLayout.vue'
import { Download, Search } from 'lucide-vue-next'
import { ElMessage } from 'element-plus'
import { workcaseAPI } from '@/api/workcase'
import type { TbWorkcaseProcessDTO } from '@/types/workcase'
import type { PageRequest, PageParam } from 'shared/types'
const dateRange = ref<[Date, Date] | null>(null)
const operationFilter = ref('')
const operatorFilter = ref('')
const searchKeyword = ref('')
const currentPage = ref(1)
// 操作类型选项
const actionOptions = [
{ value: 'info', label: '记录' },
{ value: 'assign', label: '指派' },
{ value: 'redeploy', label: '转派' },
{ value: 'repeal', label: '撤销' },
{ value: 'finish', label: '完成' }
]
// 状态
const loading = ref(false)
const dateRange = ref<[string, string] | null>(null)
const showDetailDialog = ref(false)
const selectedLog = ref<any>(null)
const selectedLog = ref<TbWorkcaseProcessDTO | null>(null)
const processLogs = ref<TbWorkcaseProcessDTO[]>([])
const logs = ref([
{ logId: 'LOG001', ticketNo: 'TK001', operation: 'create', operationName: '创建', operator: '王五', content: '创建工单,客户反映设备显示屏不亮', operationTime: '2024-12-13 10:30', ipAddress: '192.168.1.100' },
{ logId: 'LOG002', ticketNo: 'TK001', operation: 'assign', operationName: '指派', operator: '赵六', content: '将工单指派给技术人员处理', operationTime: '2024-12-13 10:35', ipAddress: '192.168.1.101' },
{ logId: 'LOG003', ticketNo: 'TK002', operation: 'create', operationName: '创建', operator: '孙七', content: '创建工单,客户反映机械故障', operationTime: '2024-12-13 09:15', ipAddress: '192.168.1.102' },
{ logId: 'LOG004', ticketNo: 'TK002', operation: 'update', operationName: '更新', operator: '王五', content: '更新工单状态为处理中', operationTime: '2024-12-13 09:45', ipAddress: '192.168.1.100' },
{ logId: 'LOG005', ticketNo: 'TK003', operation: 'complete', operationName: '完成', operator: '赵六', content: '工单处理完成,客户已确认', operationTime: '2024-12-12 14:20', ipAddress: '192.168.1.101' }
])
const filteredLogs = computed(() => {
let result = logs.value
if (operationFilter.value) {
result = result.filter(l => l.operation === operationFilter.value)
}
if (operatorFilter.value) {
result = result.filter(l => l.operator === operatorFilter.value)
}
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(l =>
l.ticketNo.toLowerCase().includes(keyword) ||
l.content.toLowerCase().includes(keyword)
)
}
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
// 筛选条件
const filter = reactive<TbWorkcaseProcessDTO>({
workcaseId: '',
action: undefined
})
const getOperationType = (operation: string) => {
// 分页
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
// 获取操作类型标签
const getActionLabel = (action?: string) => {
const map: Record<string, string> = {
create: 'success',
update: 'info',
assign: 'warning',
complete: 'success',
close: 'danger'
info: '记录',
assign: '指派',
redeploy: '转派',
repeal: '撤销',
finish: '完成'
}
return map[operation] || 'info'
return map[action || ''] || action || '-'
}
const viewDetail = (row: any) => {
// 获取操作类型标签样式
const getActionTagType = (action?: string) => {
const map: Record<string, string> = {
info: 'info',
assign: 'warning',
redeploy: '',
repeal: 'danger',
finish: 'success'
}
return map[action || ''] || 'info'
}
// 查询数据
const handleSearch = async () => {
loading.value = true
try {
const filterData: TbWorkcaseProcessDTO = { ...filter }
// 日期范围
if (dateRange.value) {
filterData.startTime = dateRange.value[0]
filterData.endTime = dateRange.value[1]
}
const pageParam: PageParam = {
pageNumber: pagination.current,
pageSize: pagination.size
}
const pageRequest: PageRequest<TbWorkcaseProcessDTO> = {
filter: filterData,
pageParam
}
const res = await workcaseAPI.getWorkcaseProcessPage(pageRequest)
if (res.success) {
processLogs.value = res.dataList || res.pageDomain?.dataList || []
pagination.total = res.pageDomain?.pageParam?.totalElements || 0
}
} catch (error) {
console.error('查询工单流程失败:', error)
ElMessage.error('查询失败')
} finally {
loading.value = false
}
}
// 查看详情
const viewDetail = (row: TbWorkcaseProcessDTO) => {
selectedLog.value = row
showDetailDialog.value = true
}
// 导出日志
const exportLogs = () => {
ElMessage.success('日志导出功')
ElMessage.success('日志导出功能开发中')
}
onMounted(() => {
handleSearch()
})
</script>
<style lang="scss" scoped>
@@ -162,4 +219,9 @@ const exportLogs = () => {
flex-direction: column;
gap: 16px;
}
</style>
.file-list {
display: flex;
flex-wrap: wrap;
}
</style>

View File

@@ -247,32 +247,6 @@ $brand-color-hover: #004488;
}
}
// 分页样式
.table-pagination {
padding: 16px 20px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #f1f5f9;
background: #fff;
}
:deep(.el-pagination) {
.el-pager {
li {
border-radius: 6px;
&.is-active {
background: $brand-color;
color: #fff;
}
}
}
.btn-prev,
.btn-next {
border-radius: 6px;
}
}
// 弹窗样式
:deep(.el-dialog) {

View File

@@ -157,6 +157,7 @@ const roomId = ref<string>('')
const workcaseId = ref<string>('')
const roomName = ref<string>('聊天室')
const guestId = ref<string>('') // 聊天室访客ID
const deviceCode = ref<string>('') // 聊天室设备代码
const commentLevel = ref<number>(0) // 已有评分
const inputText = ref<string>('')
const scrollTop = ref<number>(0)
@@ -312,6 +313,7 @@ async function refreshChatRoomInfo() {
roomName.value = roomRes.data.roomName || '聊天室'
workcaseId.value = roomRes.data.workcaseId || ''
guestId.value = roomRes.data.guestId || ''
deviceCode.value = roomRes.data.deviceCode || ''
commentLevel.value = roomRes.data.commentLevel || 0
messageTotal.value = roomRes.data.messageCount || 0
}
@@ -345,6 +347,7 @@ async function loadChatRoom() {
roomName.value = roomRes.data.roomName || '聊天室'
workcaseId.value = roomRes.data.workcaseId || ''
guestId.value = roomRes.data.guestId || ''
deviceCode.value = roomRes.data.deviceCode || ''
messageTotal.value = roomRes.data.messageCount || 0
commentLevel.value = roomRes.data.commentLevel!
}
@@ -587,8 +590,8 @@ function handleWorkcaseAction() {
}
})
} else {
// 跳转到创建工单页面
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}`
// 跳转到创建工单页面,携带 deviceCode
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}&deviceCode=${encodeURIComponent(deviceCode.value || '')}`
console.log('[handleWorkcaseAction] 创建工单跳转URL:', url)
uni.navigateTo({
url: url,

View File

@@ -95,14 +95,14 @@
<!-- 设备铭牌 -->
<view class="form-item">
<text class="form-label">设备铭牌</text>
<text class="form-label">设备铭牌<text class="required" v-if="mode === 'create'">*</text></text>
<input v-if="mode === 'create'" class="form-input" v-model="workcase.deviceNamePlate" placeholder="请输入设备铭牌"/>
<text v-else class="form-value">{{ workcase.deviceNamePlate || '-' }}</text>
</view>
<!-- 铭牌照片 -->
<view class="form-item">
<text class="form-label">铭牌照片<text class="required" v-if="mode === 'create'">*</text></text>
<text class="form-label">铭牌照片</text>
<!-- 创建模式:上传铭牌 -->
<view v-if="mode === 'create'" class="nameplate-upload">
<view v-if="workcase.deviceNamePlateImg" class="nameplate-preview" @tap="previewNameplateImage">
@@ -457,6 +457,7 @@ onLoad((options: any) => {
mode.value = 'create'
// create 模式必须从上一页带入 roomId前端先建 room 的策略)
const roomId = options.roomId || ''
const deviceCodeParam = decodeURIComponent(options.deviceCode || '')
if (!roomId) {
uni.showToast({ title: '缺少roomId无法创建工单', icon: 'none' })
// 直接退出,避免后续提交失败
@@ -485,6 +486,8 @@ onLoad((options: any) => {
phone,
userId,
roomId,
deviceCode: deviceCodeParam,
deviceNamePlate: deviceCodeParam,
device: '',
type: '',
address: '',
@@ -1102,8 +1105,8 @@ async function submitWorkcase() {
uni.showToast({ title: '请输入故障描述', icon: 'none' })
return
}
if (!workcase.deviceNamePlateImg) {
uni.showToast({ title: '请上传设备铭牌照片', icon: 'none' })
if (!workcase.deviceNamePlate) {
uni.showToast({ title: '请输入设备铭牌', icon: 'none' })
return
}
if (!workcase.imgs || workcase.imgs.length === 0) {