feat(ops): 运维小程序扫码 Tab 新增设备查询与扫码历史子页 + 工作台入口补齐
- 新增 scan/deviceList.vue(设备查询)与 scanHistory.vue(扫码历史)两个子页 - 扫码 Home / sceneMenu 流程调整,api 与 types 补充对应字段 - nav.ts 同步新增两个子页菜单项 - handlers/index 与 scan mock 注册配合调整 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e586461af7
commit
0e884b884a
@@ -82,7 +82,7 @@ import './miniprogram/ops/workbench'
|
||||
import './miniprogram/ops/stock'
|
||||
// 实施运维小程序 - 工单(列表 3 段 + 详情 + 接单/关单/维修上报)
|
||||
import './miniprogram/ops/workOrder'
|
||||
// 实施运维小程序 - 扫码(识别 / 历史 / 初始化 / 实施记录 / 维修上报 / OTA / 设备监测 5 Tab)
|
||||
// 实施运维小程序 - 扫码(识别 / 历史 / 设备列表 / 初始化 / 实施记录 / 维修上报 / OTA / 设备监测 5 Tab)
|
||||
import './miniprogram/ops/scan'
|
||||
// 实施运维小程序 - 巡检(计划列表 4 状态 + 任务详情逐项填报 + 异常转工单)
|
||||
import './miniprogram/ops/inspect'
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
* 实施运维小程序 / 扫码 mock
|
||||
* - 扫码识别设备(按编码查摘要)
|
||||
* - 扫码历史
|
||||
* - 设备列表(按客户 / 场所 / 类别 / 编码筛选,扫码兜底入口)
|
||||
* - 设备初始化 / 实施记录 / 维修上报 / OTA 升级 / 设备监测
|
||||
*/
|
||||
|
||||
import { registerMocks } from '@axios/mockBus'
|
||||
import type { ApiResponse, RequestParameter } from '@axios'
|
||||
import type {
|
||||
DeviceListFilter,
|
||||
DeviceMonitor,
|
||||
DeviceSummary,
|
||||
ImplRecordForm,
|
||||
InitForm,
|
||||
ListDeviceItem,
|
||||
MonitorRepairItem,
|
||||
MonitorUpdateItem,
|
||||
OtaTask,
|
||||
@@ -74,6 +77,20 @@ const deviceMap: Record<string, DeviceSummary> = {
|
||||
packageName: '—', softwareVersion: '—', lastHeartbeat: '—',
|
||||
pendingWorkOrders: 0, isIot: true, dataDistributeEnabled: false,
|
||||
},
|
||||
'BIKE-NJ-006': {
|
||||
id: 'dev-103', deviceCode: 'BIKE-NJ-006', deviceName: '6号智能单车(南京玄武)',
|
||||
category: 'fitness', model: 'BK-Pro',
|
||||
status: 'activated', siteName: '南京玄武 · 玄武湖健身房', tenantName: '南京健身中心',
|
||||
packageName: '健身单车App', softwareVersion: 'V1.0.0', lastHeartbeat: '2026-07-03 08:50:00',
|
||||
pendingWorkOrders: 0, isIot: true, dataDistributeEnabled: false,
|
||||
},
|
||||
'SCALE-GZ-008': {
|
||||
id: 'dev-115', deviceCode: 'SCALE-GZ-008', deviceName: '8号体脂仪(广州天河·已报废)',
|
||||
category: 'body', model: 'InBody-770',
|
||||
status: 'scrapped', siteName: '广州天河 · 天河体检站', tenantName: '广州健康管理机构',
|
||||
packageName: '体脂仪App', softwareVersion: 'V2.9.4', lastHeartbeat: '2026-06-20 16:08:42',
|
||||
pendingWorkOrders: 0, isIot: false, dataDistributeEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
/** 扫码历史(最近 6 条) */
|
||||
@@ -149,6 +166,33 @@ const genArcsoftKey = (): string => {
|
||||
}
|
||||
|
||||
registerMocks([
|
||||
// 设备列表(按客户 / 场所 / 类别 / 编码筛选,用于扫码兜底入口)
|
||||
{
|
||||
url: '/mp/ops/scan/device-list',
|
||||
method: 'GET',
|
||||
handler: (ctx): ApiResponse<ListDeviceItem[]> => {
|
||||
const { tenantName, siteName, category, deviceCode } = getParams<DeviceListFilter>(ctx.params)
|
||||
// 从 deviceMap 派生精简列表项
|
||||
const list: ListDeviceItem[] = Object.values(deviceMap).map((d) => ({
|
||||
id: d.id,
|
||||
deviceCode: d.deviceCode,
|
||||
deviceName: d.deviceName,
|
||||
category: d.category,
|
||||
status: d.status,
|
||||
siteName: d.siteName,
|
||||
tenantName: d.tenantName,
|
||||
model: d.model,
|
||||
}))
|
||||
const filtered = list.filter((d) => {
|
||||
if (tenantName && d.tenantName !== tenantName) return false
|
||||
if (siteName && d.siteName !== siteName) return false
|
||||
if (category && d.category !== category) return false
|
||||
if (deviceCode && !d.deviceCode.toLowerCase().includes(deviceCode.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
return ok('查询成功', filtered)
|
||||
},
|
||||
},
|
||||
// 扫码识别设备
|
||||
{
|
||||
url: '/mp/ops/scan/scan',
|
||||
|
||||
@@ -1084,6 +1084,16 @@ export const miniprograms: NavMiniprogram[] = [
|
||||
status: 'draft',
|
||||
},
|
||||
subPages: [
|
||||
{
|
||||
key: 'mp-ops-scan-device-list', label: '设备查询', path: 'device-list',
|
||||
component: () => import('@pages/miniprogram/ops/scan/deviceList.vue'),
|
||||
status: 'draft',
|
||||
},
|
||||
{
|
||||
key: 'mp-ops-scan-history', label: '扫码历史', path: 'scan-history',
|
||||
component: () => import('@pages/miniprogram/ops/scan/scanHistory.vue'),
|
||||
status: 'draft',
|
||||
},
|
||||
{
|
||||
key: 'mp-ops-scan-scene-menu', label: '场景操作菜单', path: 'scene-menu/:deviceId',
|
||||
component: () => import('@pages/miniprogram/ops/scan/sceneMenu.vue'),
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
<div class="scan-home__line" />
|
||||
</div>
|
||||
<div class="scan-home__tip">将设备机身二维码对准框内</div>
|
||||
<van-button class="scan-home__album" icon="photo-o" plain size="small" @click="onManualInput">
|
||||
手动输入编码
|
||||
</van-button>
|
||||
<!-- 兜底入口:手动输码(精确) + 按条件查找(模糊) -->
|
||||
<div class="scan-home__actions">
|
||||
<van-button icon="edit" plain size="small" @click="onManualInput">手动输入编码</van-button>
|
||||
<van-button icon="search" plain size="small" @click="goDeviceList">按条件查找</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模拟扫码按钮(原型无真扫码能力) -->
|
||||
@@ -22,22 +24,18 @@
|
||||
<van-button type="primary" block @click="onSimScan('CAB-NEW-001')">模拟扫码 · 待激活设备</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 历史扫码 -->
|
||||
<div class="scan-home__history">
|
||||
<div class="scan-home__history-title">最近扫码</div>
|
||||
<van-cell
|
||||
v-for="item in history"
|
||||
:key="item.id"
|
||||
:title="item.deviceName"
|
||||
:label="`${item.deviceCode} · ${item.scannedAt}`"
|
||||
is-link
|
||||
@click="onRescan(item.deviceCode)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-tag plain type="primary">{{ item.action }}</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
</div>
|
||||
<!-- 次级入口:扫码历史(抽离到独立页面,首页只保留入口,让取景框更突出) -->
|
||||
<van-cell
|
||||
class="scan-home__history-entry"
|
||||
title="扫码历史"
|
||||
label="查看最近扫过的设备"
|
||||
is-link
|
||||
@click="goScanHistory"
|
||||
>
|
||||
<template #icon>
|
||||
<van-icon name="clock-o" class="scan-home__history-entry-icon" />
|
||||
</template>
|
||||
</van-cell>
|
||||
|
||||
<!-- 手动输入对话框 -->
|
||||
<van-dialog
|
||||
@@ -54,19 +52,17 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 实施运维小程序 / 扫码首页
|
||||
* - 全屏取景框(原型无真扫码,提供模拟按钮)
|
||||
* - 历史扫码列表
|
||||
* - 手动输入设备编码兜底
|
||||
* - 全屏取景框(原型无真扫码,提供模拟按钮)
|
||||
* - 兜底入口:手动输入编码 / 按条件查找设备(跳 deviceList)
|
||||
* - 扫码历史抽离到 scan-history 子页,首页只保留入口,布局更紧凑
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showFailToast } from 'vant'
|
||||
import { listScanHistory, scanDevice } from './api'
|
||||
import type { ScanHistoryItem } from './types'
|
||||
import { scanDevice } from './api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const history = ref<ScanHistoryItem[]>([])
|
||||
const inputShow = ref(false)
|
||||
const inputCode = ref('')
|
||||
|
||||
@@ -97,29 +93,19 @@ const onInputConfirm = (): void => {
|
||||
onSimScan(inputCode.value.trim())
|
||||
}
|
||||
|
||||
const onRescan = (deviceCode: string): void => {
|
||||
onSimScan(deviceCode)
|
||||
const goDeviceList = (): void => {
|
||||
router.push('/miniprogram/ops/device-list')
|
||||
}
|
||||
|
||||
const loadHistory = (): void => {
|
||||
listScanHistory()
|
||||
.then((res) => {
|
||||
if (res.code === '00000') history.value = res.data
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => undefined)
|
||||
const goScanHistory = (): void => {
|
||||
router.push('/miniprogram/ops/scan-history')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadHistory()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.scan-home {
|
||||
min-height: 100%;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
background: var(--background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -129,7 +115,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 24px;
|
||||
padding: 32px 24px 20px;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #000 100%);
|
||||
}
|
||||
|
||||
@@ -174,9 +160,16 @@ onMounted(() => {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
&__album {
|
||||
// 取景框内两个并列兜底入口
|
||||
&__actions {
|
||||
margin-top: 16px;
|
||||
:deep(.van-button__text) { color: var(--primary); }
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.van-button) {
|
||||
--van-button-plain-text-color: var(--primary);
|
||||
--van-button-border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__sim {
|
||||
@@ -187,17 +180,14 @@ onMounted(() => {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
&__history {
|
||||
background: #fff;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
&__history-entry {
|
||||
margin-top: 8px;
|
||||
|
||||
&__history-title {
|
||||
padding: 12px 16px 6px;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background: #fff;
|
||||
&-icon {
|
||||
font-size: 20px;
|
||||
color: var(--primary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
*/
|
||||
import { getRequest, postRequest } from '@axios'
|
||||
import type {
|
||||
DeviceListFilter,
|
||||
DeviceMonitor,
|
||||
DeviceSummary,
|
||||
ImplRecordForm,
|
||||
InitForm,
|
||||
ListDeviceItem,
|
||||
OtaTask,
|
||||
OtaVersion,
|
||||
RepairForm,
|
||||
@@ -24,6 +26,12 @@ export const scanDevice = (
|
||||
export const listScanHistory = (): Promise<{ code: string; msg: string; data: ScanHistoryItem[] }> =>
|
||||
getRequest('axiosRequest', '/mp/ops/scan/history')
|
||||
|
||||
/** 设备列表(按客户 / 场所 / 类别 / 编码筛选,用于扫码兜底场景) */
|
||||
export const listDevices = (
|
||||
filters: DeviceListFilter,
|
||||
): Promise<{ code: string; msg: string; data: ListDeviceItem[] }> =>
|
||||
getRequest('axiosRequest', '/mp/ops/scan/device-list', filters)
|
||||
|
||||
/** 提交设备初始化 */
|
||||
export const submitInit = (
|
||||
params: InitForm,
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="device-list">
|
||||
<van-nav-bar title="设备查询" left-arrow @click-left="onBack" />
|
||||
|
||||
<!-- 筛选条:客户 / 场所 / 类别 三下拉 + 编码模糊搜索 -->
|
||||
<div class="device-list__filter">
|
||||
<van-dropdown-menu>
|
||||
<van-dropdown-item v-model="filter.tenantName" :options="tenantOpts" @change="onTenantChange" />
|
||||
<van-dropdown-item v-model="filter.siteName" :options="siteOpts" @change="onFilterChange" />
|
||||
<van-dropdown-item v-model="filter.category" :options="categoryOpts" @change="onFilterChange" />
|
||||
</van-dropdown-menu>
|
||||
<van-search
|
||||
v-model="filter.deviceCode"
|
||||
placeholder="搜索设备编码"
|
||||
shape="round"
|
||||
@search="onFilterChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 结果计数 -->
|
||||
<div class="device-list__count">共 {{ filtered.length }} 台设备</div>
|
||||
|
||||
<!-- 加载骨架 -->
|
||||
<div v-if="loading" class="device-list__skeleton">
|
||||
<van-skeleton v-for="n in 3" :key="n" title :row="2" />
|
||||
</div>
|
||||
|
||||
<!-- 空态 -->
|
||||
<van-empty
|
||||
v-else-if="!filtered.length"
|
||||
image="search"
|
||||
description="暂无符合条件的设备"
|
||||
/>
|
||||
|
||||
<!-- 设备卡片列表 -->
|
||||
<div v-else class="device-list__cards">
|
||||
<div
|
||||
v-for="d in filtered"
|
||||
:key="d.id"
|
||||
class="dev-card"
|
||||
@click="onPick(d.deviceCode)"
|
||||
>
|
||||
<div class="dev-card__header">
|
||||
<van-icon name="desktop-o" class="dev-card__icon" />
|
||||
<div class="dev-card__main">
|
||||
<div class="dev-card__name">{{ d.deviceName }}</div>
|
||||
<div class="dev-card__code">{{ d.deviceCode }}</div>
|
||||
</div>
|
||||
<van-tag :type="statusTagType(d.status)">{{ statusText(d.status) }}</van-tag>
|
||||
</div>
|
||||
<div class="dev-card__meta">
|
||||
<div><span>型号</span>{{ d.model }}</div>
|
||||
<div><span>类别</span>{{ categoryText(d.category) }}</div>
|
||||
<div><span>客户</span>{{ d.tenantName }}</div>
|
||||
<div><span>场所</span>{{ d.siteName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 实施运维小程序 / 设备查询
|
||||
* - 扫码兜底入口:不在现场 / 二维码损坏时,按客户 / 场所 / 类别 / 编码快速定位设备
|
||||
* - 选中设备 → 跳场景操作菜单 sceneMenu(deviceCode),复用 9 项场景操作(零业务重复)
|
||||
* - 原型数据量小(运维工程师负责的设备有限),首次拉全集 + 本地 distinct 选项 + 本地过滤
|
||||
* 研发接手时 filters 参数契约保留,后端如需服务端筛选可直接启用
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showFailToast } from 'vant'
|
||||
import { listDevices } from './api'
|
||||
import type { DeviceCategory, DeviceStatus, ListDeviceItem } from './types'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const all = ref<ListDeviceItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const filter = reactive<{
|
||||
tenantName: string
|
||||
siteName: string
|
||||
category: DeviceCategory | ''
|
||||
deviceCode: string
|
||||
}>({
|
||||
tenantName: '',
|
||||
siteName: '',
|
||||
category: '',
|
||||
deviceCode: '',
|
||||
})
|
||||
|
||||
/** 客户下拉(从全集 distinct) */
|
||||
const tenantOpts = computed(() => [
|
||||
{ text: '全部客户', value: '' },
|
||||
...Array.from(new Set(all.value.map((d) => d.tenantName))).map((n) => ({ text: n, value: n })),
|
||||
])
|
||||
|
||||
/** 场所下拉(随客户联动:选中客户时只展示该客户下的场所) */
|
||||
const siteOpts = computed(() => {
|
||||
const scoped = filter.tenantName
|
||||
? all.value.filter((d) => d.tenantName === filter.tenantName)
|
||||
: all.value
|
||||
return [
|
||||
{ text: '全部场所', value: '' },
|
||||
...Array.from(new Set(scoped.map((d) => d.siteName))).map((n) => ({ text: n, value: n })),
|
||||
]
|
||||
})
|
||||
|
||||
/** 设备类别下拉(固定 5 大顶级分类,与 DeviceCategory 对齐) */
|
||||
const categoryOpts: Array<{ text: string; value: DeviceCategory | '' }> = [
|
||||
{ text: '全部类别', value: '' },
|
||||
{ text: '智养餐饮设备', value: 'catering' },
|
||||
{ text: '体重测量设备', value: 'body' },
|
||||
{ text: '智能穿戴设备', value: 'wearable' },
|
||||
{ text: '健身器材设备', value: 'fitness' },
|
||||
{ text: '健康监测设备', value: 'health' },
|
||||
]
|
||||
|
||||
/** 本地过滤(原型数据量小,直接内存过滤避免每次切下拉都发请求) */
|
||||
const filtered = computed<ListDeviceItem[]>(() => {
|
||||
const kw = filter.deviceCode.trim().toLowerCase()
|
||||
return all.value.filter((d) => {
|
||||
if (filter.tenantName && d.tenantName !== filter.tenantName) return false
|
||||
if (filter.siteName && d.siteName !== filter.siteName) return false
|
||||
if (filter.category && d.category !== filter.category) return false
|
||||
if (kw && !d.deviceCode.toLowerCase().includes(kw)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const statusText = (s: DeviceStatus): string => ({
|
||||
inactive: '未激活',
|
||||
activated: '已激活',
|
||||
inUse: '在用',
|
||||
scrapped: '已报废',
|
||||
})[s]
|
||||
|
||||
const statusTagType = (s: DeviceStatus): 'primary' | 'warning' | 'success' | 'default' =>
|
||||
s === 'inUse' ? 'success' : s === 'activated' ? 'primary' : s === 'inactive' ? 'warning' : 'default'
|
||||
|
||||
const categoryText = (c: DeviceCategory): string => ({
|
||||
catering: '智养餐饮',
|
||||
body: '体重测量',
|
||||
wearable: '智能穿戴',
|
||||
fitness: '健身器材',
|
||||
health: '健康监测',
|
||||
})[c]
|
||||
|
||||
/** 切客户后联动:已选场所若不在新客户范围内,清空 */
|
||||
const onTenantChange = (): void => {
|
||||
if (filter.siteName && !siteOpts.value.some((o) => o.value === filter.siteName)) {
|
||||
filter.siteName = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 编码 search 触发(本地过滤,computed 自动响应,这里仅占位避免 lint) */
|
||||
const onFilterChange = (): void => undefined
|
||||
|
||||
const onPick = (deviceCode: string): void => {
|
||||
router.push(`/miniprogram/ops/scene-menu/${deviceCode}`)
|
||||
}
|
||||
|
||||
const onBack = (): void => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadAll = (): void => {
|
||||
loading.value = true
|
||||
listDevices({})
|
||||
.then((res) => {
|
||||
if (res.code === '00000') all.value = res.data
|
||||
else showFailToast(res.msg)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.device-list {
|
||||
min-height: 100%;
|
||||
background: var(--background);
|
||||
padding-bottom: 16px;
|
||||
|
||||
&__filter {
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
&__count {
|
||||
padding: 8px 16px;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
&__skeleton {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
|
||||
.van-skeleton {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__cards {
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dev-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 24px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--font-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__code {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
|
||||
span {
|
||||
color: var(--text-placeholder);
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="scan-history">
|
||||
<van-nav-bar title="扫码历史" left-arrow @click-left="onBack" />
|
||||
|
||||
<van-empty v-if="!list.length" image="search" description="暂无扫码历史" />
|
||||
<van-cell-group v-else inset class="scan-history__list">
|
||||
<van-cell
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:title="item.deviceName"
|
||||
:label="`${item.deviceCode} · ${item.scannedAt}`"
|
||||
is-link
|
||||
@click="onPick(item.deviceCode)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-tag plain type="primary">{{ item.action }}</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 实施运维小程序 / 扫码历史
|
||||
* - 从 scan/Home.vue 抽离,首页只放入口链接,让取景框更突出
|
||||
* - 点击历史项 → 跳 sceneMenu(deviceCode),等价于重新扫码
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { listScanHistory } from './api'
|
||||
import type { ScanHistoryItem } from './types'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const list = ref<ScanHistoryItem[]>([])
|
||||
|
||||
const onPick = (deviceCode: string): void => {
|
||||
router.push(`/miniprogram/ops/scene-menu/${deviceCode}`)
|
||||
}
|
||||
|
||||
const onBack = (): void => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const loadList = (): void => {
|
||||
listScanHistory()
|
||||
.then((res) => {
|
||||
if (res.code === '00000') list.value = res.data
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => undefined)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.scan-history {
|
||||
min-height: 100%;
|
||||
background: var(--background);
|
||||
padding-top: 8px;
|
||||
|
||||
&__list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@
|
||||
/**
|
||||
* 实施运维小程序 / 扫码场景操作菜单
|
||||
* - 设备摘要卡(编码 / 型号 / 状态 / 待处理工单红点)
|
||||
* - 9 项操作按设备状态过滤显示(6 项运维 + 3 项库存作业)
|
||||
* - 6 项运维操作按设备状态过滤显示(库存作业请走工作台「库存作业」入口)
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -77,8 +77,6 @@ interface SceneAction {
|
||||
icon: string
|
||||
path: (deviceId: string) => string
|
||||
statuses: DeviceStatus[]
|
||||
/** 库存相关场景独立于设备生命周期,可与任意状态并存 */
|
||||
isStock?: boolean
|
||||
}
|
||||
|
||||
const ALL_ACTIONS: SceneAction[] = [
|
||||
@@ -112,25 +110,6 @@ const ALL_ACTIONS: SceneAction[] = [
|
||||
icon: 'eye-o', path: (id) => `/miniprogram/ops/monitor/${id}`,
|
||||
statuses: ['activated', 'inUse', 'scrapped'],
|
||||
},
|
||||
// 库存作业 3 个场景(扫码进入时自动带入 deviceCode)
|
||||
{
|
||||
key: 'stock-inbound', label: '扫码入库', desc: '将本设备登记到收货入库清单',
|
||||
icon: 'after-sale', path: (code) => `/miniprogram/ops/stock-inbound?deviceCode=${code}`,
|
||||
statuses: ['inactive', 'activated', 'inUse', 'scrapped'],
|
||||
isStock: true,
|
||||
},
|
||||
{
|
||||
key: 'stock-outbound', label: '扫码出库', desc: '将本设备加入装机出库清单',
|
||||
icon: 'logistics', path: (code) => `/miniprogram/ops/stock-outbound?deviceCode=${code}`,
|
||||
statuses: ['inactive', 'activated', 'inUse', 'scrapped'],
|
||||
isStock: true,
|
||||
},
|
||||
{
|
||||
key: 'stock-inventory', label: '扫码盘点', desc: '在当前盘点单中标记本设备实盘结果',
|
||||
icon: 'balance-list-o', path: (code) => `/miniprogram/ops/stock-inventory?deviceCode=${code}&id=inv-006`,
|
||||
statuses: ['inactive', 'activated', 'inUse', 'scrapped'],
|
||||
isStock: true,
|
||||
},
|
||||
]
|
||||
|
||||
const availableActions = computed<SceneAction[]>(() => {
|
||||
|
||||
@@ -167,3 +167,27 @@ export interface DeviceMonitor {
|
||||
repairs: MonitorRepairItem[]
|
||||
updates: MonitorUpdateItem[]
|
||||
}
|
||||
|
||||
/** 设备列表项(查询场景精简版,剔除心跳 / 工单 / IOT 等监测字段,仅用于列表展示) */
|
||||
export interface ListDeviceItem {
|
||||
id: string
|
||||
deviceCode: string
|
||||
deviceName: string
|
||||
category: DeviceCategory
|
||||
status: DeviceStatus
|
||||
siteName: string
|
||||
tenantName: string
|
||||
model: string
|
||||
}
|
||||
|
||||
/** 设备列表筛选条件(用于"按客户 / 场所 / 类别 / 编码"快速定位设备) */
|
||||
export interface DeviceListFilter {
|
||||
/** 客户(租户)名,空表示不限 */
|
||||
tenantName?: string
|
||||
/** 场所名,空表示不限 */
|
||||
siteName?: string
|
||||
/** 设备顶级分类,空表示不限 */
|
||||
category?: DeviceCategory
|
||||
/** 设备编码模糊匹配 */
|
||||
deviceCode?: string
|
||||
}
|
||||
|
||||
@@ -103,6 +103,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备查询区(扫码兜底入口:不在现场 / 二维码损坏时按条件定位设备) -->
|
||||
<div class="ops-workbench__section">
|
||||
<div class="ops-workbench__section-title">设备查询</div>
|
||||
<div class="dev-search-card" @click="goDeviceList">
|
||||
<van-icon name="search" class="dev-search-card__icon" />
|
||||
<div class="dev-search-card__main">
|
||||
<div class="dev-search-card__label">按客户 / 场所 / 类别 / 编码查找设备</div>
|
||||
<div class="dev-search-card__sub">不在现场或二维码损坏时,从列表快速定位</div>
|
||||
</div>
|
||||
<van-icon name="arrow" class="dev-search-card__arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -141,6 +154,10 @@ const goPath = (path: string): void => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const goDeviceList = (): void => {
|
||||
router.push('/miniprogram/ops/device-list')
|
||||
}
|
||||
|
||||
const goAlarmList = (): void => {
|
||||
router.push('/miniprogram/ops/alarm-list')
|
||||
}
|
||||
@@ -454,4 +471,47 @@ onMounted(() => {
|
||||
.quick-entry__icon { color: #595959; }
|
||||
}
|
||||
}
|
||||
|
||||
.dev-search-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 3px solid var(--primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 24px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&__sub {
|
||||
margin-top: 2px;
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 16px;
|
||||
color: var(--text-placeholder);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user