This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
<template>
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="设备详情" :xloading="true" @right-click="onMore">
<template v-slot:right>
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
</template>
<view class="hr-body">
<!-- ============ 概览 ============ -->
<hr-card>
<view class="hr-row">
<view class="hr-avt hr-avt-lg dd-avt">
<text>{{ device.icon }}</text>
</view>
<view class="hr-flex1">
<text class="dd-name">{{ device.name }}</text>
<text class="dd-meta">{{ device.meta }}</text>
<view style="margin-top:7px">
<hr-pc :level="2" :text="device.status"></hr-pc>
</view>
</view>
</view>
</hr-card>
<!-- ============ 借用前自检 ============ -->
<hr-sec :title="'借用前自检(5 项缺一不可)'" :more="checkedCount + ' / 5'"></hr-sec>
<hr-card variant="list">
<view v-for="(c, i) in checks" :key="i" :class="c.checked ? 'dd-ck dd-ck-on' : 'dd-ck'"
@click="toggleCheck(i)">
<view :class="c.checked ? 'dd-ck-box dd-ck-box-on' : 'dd-ck-box'">
<x-icon v-if="c.checked" name="check-line" color="#FFFFFF" font-size="14"></x-icon>
</view>
<view class="hr-flex1">
<text class="dd-ck-t">{{ c.title }}</text>
<text class="dd-ck-s">{{ c.sub }}</text>
</view>
</view>
</hr-card>
<!-- ============ 维护记录 ============ -->
<hr-sec title="维护记录" :first="false"></hr-sec>
<hr-card>
<hr-timeline :nodes="device.maintain"></hr-timeline>
</hr-card>
<hr-tip style="margin-top:14px" theme="warn" icon="⚠"
text="发现设备异常可直接一键报修,系统将自动流转至设备科并暂停借出。"></hr-tip>
<view style="height:14px"></view>
</view>
<template v-slot:footer>
<view class="hr-fixedbar">
<view class="hr-btn hr-btn-ghost dd-foot-report" @click="onReport">
<x-icon name="error-warning-line" color="#1F2937" font-size="16"></x-icon>
<text class="dd-foot-t">报修</text>
</view>
<view :class="allChecked ? 'hr-btn hr-btn-primary' : 'hr-btn hr-btn-disabled'" style="flex:1"
@click="onBorrow">
<text :class="allChecked ? 'dd-foot-t2' : 'dd-foot-t3'">
{{ allChecked ? '确认自检并借用' : '请完成 5 项自检' }}
</text>
</view>
</view>
</template>
</hr-page>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { deviceDetail } from '@/mock/index.uts'
import { HrDeviceCheck } from '@/mock/types.uts'
import { navBack } from '@/utils/nav.uts'
/**
* s32 设备详情
* 借用前 5 项自检缺一不可,全部确认后才可借出。
*/
const device = deviceDetail
const checks = ref<HrDeviceCheck[]>([])
// 深拷贝一份,避免污染 mock 数据
const arr : HrDeviceCheck[] = []
for (let i = 0; i < device.checkList.length; i++) {
const c = device.checkList[i]
arr.push({ title: c.title, sub: c.sub, checked: c.checked } as HrDeviceCheck)
}
checks.value = arr
const checkedCount = computed(() : number => {
let n = 0
for (let i = 0; i < checks.value.length; i++) {
if (checks.value[i].checked) { n++ }
}
return n
})
const allChecked = computed(() : boolean => {
return checkedCount.value == checks.value.length
})
function toggleCheck(i : number) : void {
const list : HrDeviceCheck[] = []
for (let k = 0; k < checks.value.length; k++) {
const c = checks.value[k]
list.push({
title: c.title, sub: c.sub, checked: k == i ? !c.checked : c.checked
} as HrDeviceCheck)
}
checks.value = list
}
function onBorrow() : void {
if (!allChecked.value) {
uni.showToast({ title: '请先完成 5 项自检', icon: 'none' })
return
}
uni.showLoading({ title: '提交中' })
setTimeout(() => {
uni.hideLoading()
uni.showModal({
title: '借用成功',
content: `${device.name}\n预计归还时间:3 天后(09-27)\n已生成借用单,逾期将自动提醒。`,
showCancel: false,
success: (res) => {
navBack()
}
})
}, 700)
}
function onReport() : void {
uni.showModal({
title: '一键报修',
editable: true,
placeholderText: '请描述设备异常,如:屏幕无法点亮',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '已流转至设备科', icon: 'success' })
}
}
})
}
function onMore() : void {
uni.showToast({ title: '设备档案 / 借还记录', icon: 'none' })
}
</script>
<style>
.dd-avt {
margin-right: 26rpx;
background-color: #2563EB;
}
.dd-name {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.dd-meta {
font-size: 25.8rpx;
color: #6B7280;
margin-top: 10rpx;
}
.dd-ck {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 22rpx;
padding-bottom: 22rpx;
border-bottom: 2rpx solid #F3F4F6;
}
.dd-ck-on {
/* 选中态体现在勾选框 */
}
.dd-ck-box {
width: 44rpx;
height: 44rpx;
border-radius: 14rpx;
border: 3rpx solid #D1D5DB;
background-color: #FFFFFF;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 22rpx;
}
.dd-ck-box-on {
border: 3rpx solid #10B981;
background-color: #10B981;
}
.dd-ck-t {
font-size: 30.2rpx;
font-weight: bold;
color: #1F2937;
}
.dd-ck-s {
font-size: 23.6rpx;
color: #9CA3AF;
margin-top: 4rpx;
}
.dd-foot-report {
flex: 0 0 220rpx;
margin-right: 18rpx;
}
.dd-foot-t {
font-size: 31.4rpx;
font-weight: bold;
color: #1F2937;
margin-left: 10rpx;
}
.dd-foot-t2 {
font-size: 32.4rpx;
font-weight: bold;
color: #FFFFFF;
}
.dd-foot-t3 {
font-size: 32.4rpx;
font-weight: bold;
color: #9CA3AF;
}
</style>
+277
View File
@@ -0,0 +1,277 @@
<template>
<view class="hr-page" style="background-color:#F3F4F6">
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
<view class="wk-nav">
<text class="wk-nav-t">设备借用</text>
<view class="wk-nav-a" @click="onLedger">
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="16"></x-icon>
<text class="wk-nav-a-t">台账</text>
</view>
</view>
<view class="hr-flow">
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
<view class="hr-body">
<!-- 状态筛选(xTabs -->
<x-tabs :list="tabList" :model-value="activeId" item-width="33.33%" height="84rpx" font-size="28rpx"
active-font-size="28rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
title-padding="0" :is-item-center="true" @change="onTabChange"></x-tabs>
<view style="margin-top:20rpx">
<view v-for="(d, i) in shown" :key="d.id" class="dv-card" @click="openDetail(d)">
<view class="dv-ic" :style="{ backgroundColor: d.iconBg }">
<x-icon :name="d.icon" :color="d.iconColor" font-size="44rpx"></x-icon>
</view>
<view class="hr-flex1">
<text class="dv-name">{{ d.name }}</text>
<text class="dv-meta">{{ d.meta }}</text>
</view>
<view class="dv-st" :style="stStyle(d.theme)">
<text class="dv-st-t" :style="stTextStyle(d.theme)">{{ d.status }}</text>
</view>
</view>
</view>
<hr-empty v-if="shown.length == 0" icon="⚙" title="该状态下暂无设备"
sub="切换上方分段查看其它状态的设备"></hr-empty>
<hr-empty v-if="shown.length == 0" icon="⚙" title="该状态下暂无设备"
sub="切换上方分段查看其它状态的设备"></hr-empty>
<hr-sec title="设备台账" more="全部 24 台 "></hr-sec>
<hr-card variant="list">
<hr-lrow icon="check-line" theme="grn" title="状态正常" sub="可随时借用">
<template v-slot:right>
<text class="dv-v dv-v-grn">18 台</text>
</template>
</hr-lrow>
<hr-lrow icon="file-list-3-line" theme="amb" title="借用中" sub="预计陆续归还">
<template v-slot:right>
<text class="dv-v dv-v-amb">4 台</text>
</template>
</hr-lrow>
<hr-lrow icon="monitor-line" theme="red" title="维修 / 保养中" sub="暂不可借" :is-last="true">
<template v-slot:right>
<text class="dv-v dv-v-red">2 台</text>
</template>
</hr-lrow>
</hr-card>
<view style="height:28rpx"></view>
</view>
</scroll-view>
<!-- 加载中覆盖层(组件库 xLoading -->
<view v-if="loading" class="hr-loading-mask">
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
text-color="#6B7280"></x-loading>
</view>
</view>
<view class="hr-fixedbar">
<view class="hr-btn hr-btn-primary" style="flex:1" @click="onScanBorrow">
<x-icon name="scan-line" color="#FFFFFF" font-size="17"></x-icon>
<text class="dv-bt">扫码借用设备</text>
</view>
</view>
<hr-tabbar :active="2"></hr-tabbar>
</view>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { deviceList } from '@/mock/index.uts'
import { HrDevice } from '@/mock/types.uts'
import { navTo } from '@/utils/nav.uts'
import { mockRequest } from '@/utils/request.uts'
import { TABS_ITEM_INFO, TABS_ITEM } from '@/uni_modules/tmx-ui/interface.uts'
/**
* s31 设备借用(仓管 Tab「设备」)
* 按「可借用 / 借用中 / 全部」分段筛选,维修中设备不可借。
*/
const loading = ref(true)
const segIds : string[] = ['0', '1', '2']
const activeId = ref('0')
/** xTabs 数据 */
const tabList : TABS_ITEM_INFO[] = [
{ title: '可借用', id: '0' } as TABS_ITEM_INFO,
{ title: '借用中', id: '1' } as TABS_ITEM_INFO,
{ title: '全部', id: '2' } as TABS_ITEM_INFO
]
const seg = computed(() : number => {
const i = parseInt(activeId.value)
return isNaN(i) ? 0 : i
})
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(不是 TABS_ITEM_INFO),形参类型必须一致
function onTabChange(item : TABS_ITEM, index : number) : void {
activeId.value = item.id == '' ? index.toString() : item.id
}
onLoad(() => {
mockRequest(() => {
loading.value = false
})
})
const list = ref<HrDevice[]>(deviceList)
const shown = computed(() : HrDevice[] => {
const arr : HrDevice[] = []
for (let i = 0; i < list.value.length; i++) {
const d = list.value[i]
if (seg.value == 0 && d.status != '可借') { continue }
if (seg.value == 1 && d.status != '借用中') { continue }
arr.push(d)
}
return arr
})
function stStyle(theme : string) : string {
if (theme == 'grn') { return 'background-color:#ECFDF5' }
if (theme == 'amb') { return 'background-color:#FEF3C7' }
return 'background-color:#FEF2F2'
}
function stTextStyle(theme : string) : string {
if (theme == 'grn') { return 'color:#059669' }
if (theme == 'amb') { return 'color:#92400E' }
return 'color:#DC2626'
}
function openDetail(d : HrDevice) : void {
navTo('/pages/warehouse/device-detail?id=' + d.id)
}
function onScanBorrow() : void {
uni.showToast({ title: '对准设备二维码开始借用', icon: 'none' })
}
function onLedger() : void {
uni.showToast({ title: '设备台账(24 台)', icon: 'none' })
}
</script>
<style>
.wk-nav {
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #FFFFFF;
border-bottom: 2rpx solid #F3F4F6;
}
.wk-nav-t {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.wk-nav-a {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
}
.wk-nav-a-t {
font-size: 29.2rpx;
color: #6B7280;
margin-left: 8rpx;
}
.dv-seg-t {
font-size: 29.2rpx;
color: #6B7280;
}
.dv-seg-t-on {
color: #2563EB;
font-weight: bold;
}
.dv-card {
display: flex;
flex-direction: row;
align-items: center;
background-color: #FFFFFF;
border: 2rpx solid #E5E7EB;
border-radius: 26rpx;
padding: 24rpx 24rpx 24rpx 24rpx;
margin-bottom: 18rpx;
}
.dv-ic {
width: 104rpx;
height: 104rpx;
border-radius: 26rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 22rpx;
}
.dv-ic-t {
font-size: 49.2rpx;
}
.dv-name {
font-size: 30.2rpx;
font-weight: bold;
color: #1F2937;
lines: 1;
}
.dv-meta {
font-size: 23.6rpx;
color: #9CA3AF;
margin-top: 6rpx;
}
.dv-st {
border-radius: 12rpx;
padding: 6rpx 16rpx 6rpx 16rpx;
margin-left: 16rpx;
}
.dv-st-t {
font-size: 23.6rpx;
font-weight: bold;
}
.dv-v {
font-size: 31.4rpx;
font-weight: bold;
margin-left: 16rpx;
}
.dv-v-grn {
color: #059669;
}
.dv-v-amb {
color: #D97706;
}
.dv-v-red {
color: #DC2626;
}
.dv-bt {
font-size: 32.4rpx;
font-weight: bold;
color: #FFFFFF;
margin-left: 12rpx;
}
</style>
+273
View File
@@ -0,0 +1,273 @@
<template>
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="物资详情" :xloading="true" @right-click="onMore">
<template v-slot:right>
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
</template>
<view class="hr-body">
<!-- ============ 概览 ============ -->
<hr-card>
<view class="hr-row">
<view class="gd-ring">
<x-circle-progress :size="76" :line-width="8" color="#F3F4F6" active-color="#F59E0B"
:model-value="ringPercent" :show-label="false" :duration="600"></x-circle-progress>
<view class="gd-ring-label">
<text class="gd-ring-b">{{ goods.stock }}</text>
<text class="gd-ring-u">{{ goods.unit }}</text>
</view>
</view>
<view class="hr-flex1">
<text class="gd-name">{{ goods.name }}</text>
<text class="gd-code">编码 {{ goods.code }} · {{ goods.spec }}</text>
<view style="margin-top:7px">
<hr-pc :level="0" :text="goods.statusLabel"></hr-pc>
</view>
</view>
</view>
</hr-card>
<!-- ============ 库存信息 ============ -->
<hr-sec title="库存信息"></hr-sec>
<hr-card variant="list">
<hr-lrow icon="▦" title="当前库存" :sub="'预警线 ' + goods.warnLine + ' ' + goods.unit">
<template v-slot:right>
<text class="gd-v gd-v-red">{{ goods.stock }} {{ goods.unit }}</text>
</template>
</hr-lrow>
<hr-lrow icon="⇅" theme="pri" title="近 30 天出库" :sub="goods.dailyUse">
<template v-slot:right>
<text class="gd-v">{{ goods.out30 }} {{ goods.unit }}</text>
</template>
</hr-lrow>
<hr-lrow icon="▤" theme="grn" title="最近入库" :sub="goods.lastIn" :is-last="true">
<template v-slot:right>
<text class="gd-v gd-v-grn">{{ goods.lastInQty }}</text>
</template>
</hr-lrow>
</hr-card>
<!-- ============ 消耗趋势 ============ -->
<hr-sec title="30 天消耗趋势" more="查看明细 "></hr-sec>
<hr-card>
<view class="gd-chart">
<view v-for="(h, i) in barHeights" :key="i" class="gd-bar-wrap">
<view class="gd-bar" :style="barStyle(i, h)"></view>
</view>
</view>
<view class="gd-xaxis">
<text v-for="(x, i) in xLabels" :key="i" class="gd-x">{{ x }}</text>
</view>
<view class="gd-legend">
<view class="gd-lg">
<view class="gd-lg-d" style="background-color:#EFF6FF"></view>
<text class="gd-lg-t">正常区间</text>
</view>
<view class="gd-lg">
<view class="gd-lg-d" style="background-color:#F59E0B"></view>
<text class="gd-lg-t">接近预警</text>
</view>
<view class="gd-lg">
<view class="gd-lg-d" style="background-color:#DC2626"></view>
<text class="gd-lg-t">低于预警线</text>
</view>
</view>
</hr-card>
<hr-tip style="margin-top:14px" theme="warn" icon="⚠"
:text="'按当前消耗速度(' + goods.dailyUse + '),库存预计 ' + daysLeft + ' 天后耗尽,建议尽快补货。'"></hr-tip>
<view class="hr-btn hr-btn-primary hr-btn-lg" style="margin-top:12px" @click="onRestock">
<x-icon name="add-line" color="#FFFFFF" font-size="17"></x-icon>
<text class="gd-bt">发起补货申请</text>
</view>
<view style="height:14px"></view>
</view>
</hr-page>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { goodsDetail } from '@/mock/index.uts'
import { navTo } from '@/utils/nav.uts'
/**
* s25 物资详情
* 库存环 + 库存信息 + 30 天消耗趋势 + 补货入口。
*/
const goods = goodsDetail
const xLabels : string[] = ['08-24', '08-31', '09-07', '09-14', '09-21']
const ringPercent = computed(() : number => {
// 库存 / (预警线 × 2.5) —— 8 / 50 ≈ 16%,与设计稿 18% 量级一致
return Math.round(goods.stock * 100 / (goods.warnLine * 2.5))
})
const barHeights = computed(() : number[] => {
const arr : number[] = []
for (let i = 0; i < goods.trend.length; i++) {
arr.push(Math.round(176 * goods.trend[i] / 100))
}
return arr
})
const daysLeft = computed(() : number => {
const daily = goods.out30 / 30
if (daily <= 0) { return 0 }
return Math.floor(goods.stock / daily)
})
function barStyle(i : number, h : number) : string {
let color = '#EFF6FF'
if (i == goods.trend.length - 1) { color = '#DC2626' }
else if (i == goods.trend.length - 2) { color = '#F59E0B' }
return `height:${h}rpx;background-color:${color};`
}
function onMore() : void {
uni.showToast({ title: '出入库流水 / 编辑物资', icon: 'none' })
}
function onRestock() : void {
uni.showModal({
title: '发起补货申请',
content: `物资:${goods.name}\n建议补货量:40 ${goods.unit}(约 2 周用量)`,
cancelText: '取消',
confirmText: '提交申请',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '补货申请已提交', icon: 'success' })
setTimeout(() => {
navTo('/pages/message/index')
}, 700)
}
}
})
}
</script>
<style>
.gd-ring {
width: 152rpx;
height: 152rpx;
position: relative;
margin-right: 26rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.gd-ring-label {
position: absolute;
left: 0rpx;
top: 0rpx;
width: 152rpx;
height: 152rpx;
flex-direction: row;
align-items: center;
justify-content: center;
}
.gd-ring-b {
font-size: 42.6rpx;
font-weight: bold;
color: #1F2937;
}
.gd-ring-u {
font-size: 22.4rpx;
color: #9CA3AF;
margin-left: 2rpx;
}
.gd-name {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.gd-code {
font-size: 25.8rpx;
color: #6B7280;
margin-top: 10rpx;
}
.gd-v {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
margin-left: 16rpx;
}
.gd-v-red {
color: #DC2626;
}
.gd-v-grn {
color: #059669;
}
/* ---------- 趋势图 ---------- */
.gd-chart {
display: flex;
flex-direction: row;
align-items: flex-end;
height: 176rpx;
}
.gd-bar-wrap {
flex: 1;
justify-content: flex-end;
margin-right: 10rpx;
}
.gd-bar {
border-radius: 8rpx 8rpx 0rpx 0rpx;
}
.gd-xaxis {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 14rpx;
}
.gd-x {
font-size: 22.4rpx;
color: #9CA3AF;
}
.gd-legend {
display: flex;
flex-direction: row;
margin-top: 20rpx;
}
.gd-lg {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 28rpx;
}
.gd-lg-d {
width: 20rpx;
height: 20rpx;
border-radius: 6rpx;
margin-right: 10rpx;
}
.gd-lg-t {
font-size: 23.6rpx;
color: #6B7280;
}
.gd-bt {
font-size: 35.8rpx;
font-weight: bold;
color: #FFFFFF;
margin-left: 12rpx;
}
</style>
+397
View File
@@ -0,0 +1,397 @@
<template>
<view class="hr-page" style="background-color:#F3F4F6">
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
<view class="wk-nav">
<text class="wk-nav-t">扫码出入库</text>
<view class="wk-nav-a" @click="toggleFlash">
<x-icon name="flashlight-line" :color="flash ? '#2563EB' : '#6B7280'" font-size="16"></x-icon>
<text class="wk-nav-a-t">手电</text>
</view>
</view>
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
<view class="hr-body">
<!-- ============ 取景框 ============ -->
<view class="sc-box">
<!-- 真实相机(APP / H5 -->
<!-- #ifdef APP || WEB -->
<x-mlkit-scannig-u :flash="flash" :autoOpenCamera="true" :cameraWidth="1080"
:cameraeiHght="1080" style="width:100%;height:100%;position:absolute;left:0;top:0"
@scan="onScan"></x-mlkit-scannig-u>
<!-- #endif -->
<view class="sc-frame">
<view class="sc-cf sc-cf-tl"></view>
<view class="sc-cf sc-cf-tr"></view>
<view class="sc-cf sc-cf-bl"></view>
<view class="sc-cf sc-cf-br"></view>
<view class="sc-line" :style="laserStyle"></view>
</view>
<text class="sc-tip">将条码对准框内 · 自动识别</text>
</view>
<!-- ============ 业务动作 ============ -->
<view class="hr-seg" style="margin-top:12px">
<view v-for="(m, i) in modes" :key="i" :class="i == modeIdx ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'"
@click="modeIdx = i">
<text :class="i == modeIdx ? 'sc-seg-t sc-seg-t-on' : 'sc-seg-t'">{{ m }}</text>
</view>
</view>
<!-- ============ 识别反馈(三态) ============ -->
<hr-sec title="识别反馈" :first="false"></hr-sec>
<view v-if="feedbacks.length == 0" class="sc-empty">
<x-icon name="scan-line" color="#9CA3AF" font-size="24"></x-icon>
<text class="sc-empty-t">等待扫码…</text>
<text class="sc-empty-s">识别后会在此即时给出 ✅ 成功 / ⚠ 库存不足 / ❌ 未登记 三种反馈</text>
</view>
<view v-for="(f, i) in feedbacks" :key="i" :class="feedbackClass(f.theme)">
<view class="sc-st-ic" :style="{ backgroundColor: feedbackColor(f.theme) }">
<text class="sc-st-ic-t">{{ f.icon }}</text>
</view>
<view class="hr-flex1">
<text class="sc-st-b">{{ f.title }}</text>
<text class="sc-st-s">{{ f.sub }}</text>
</view>
</view>
<!-- ============ 本次连续扫入 ============ -->
<hr-sec title="本次连续扫入" :more="records.length + ' 条'"></hr-sec>
<hr-card variant="list">
<hr-lrow v-for="(r, i) in records" :key="i" :icon="r.icon"
:theme="r.theme == 'grn' ? 'grn' : (r.theme == 'amb' ? 'amb' : 'red')" :title="r.name"
:sub="r.time + ' · ' + r.delta" :value="r.status"
:value-theme="r.theme == 'grn' ? 'grn' : 'amb'" :value-bold="true"
:is-last="i == records.length - 1" @click="onRecord(r)"></hr-lrow>
</hr-card>
<view style="height:14px"></view>
</view>
</scroll-view>
<hr-tabbar :active="0"></hr-tabbar>
</view>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { scanFeedbacks, scanRecords } from '@/mock/index.uts'
import { HrScanFeedback, HrScanRecord } from '@/mock/types.uts'
/**
* s23 扫码出入库(仓管 Tab「扫码」)
* 连续扫码三态即时反馈:✅ 入库成功 / ⚠ 库存不足 / ❌ 未登记。
*/
const modes : string[] = ['入库', '出库', '盘点']
const modeIdx = ref(0)
const flash = ref(false)
const feedbacks = ref<HrScanFeedback[]>([
scanFeedbacks[0], scanFeedbacks[1], scanFeedbacks[2]
])
const records = ref<HrScanRecord[]>([
scanRecords[0], scanRecords[1], scanRecords[2]
])
let seq = 3
function shortCode(code : string) : string {
if (code.length <= 10) { return code }
return code.substring(0, 10)
}
function nowTime() : string {
const d = new Date()
const hh = d.getHours() < 10 ? '0' + d.getHours().toString() : d.getHours().toString()
const mm = d.getMinutes() < 10 ? '0' + d.getMinutes().toString() : d.getMinutes().toString()
return hh + ':' + mm
}
/** 依据条码特征给出三态反馈(mock 规则,后续替换为接口返回) */
function handleCode(code : string) : void {
seq = seq + 1
const fb : HrScanFeedback = {
theme: 'ok', icon: '✓',
title: '一次性医用口罩 · ' + modes[modeIdx.value] + '成功',
sub: '规格 50 只/盒 · 现存 320 盒 · 张明远 · 编码 ' + shortCode(code)
} as HrScanFeedback
const rec : HrScanRecord = {
name: '一次性医用口罩', time: nowTime(), delta: '+1 盒', status: '已入库', theme: 'grn', icon: '✓'
} as HrScanRecord
const fbArr : HrScanFeedback[] = []
fbArr.push(fb)
for (let i = 0; i < feedbacks.value.length; i++) {
if (i < 2) { fbArr.push(feedbacks.value[i]) }
}
feedbacks.value = fbArr
const recArr : HrScanRecord[] = []
recArr.push(rec)
for (let i = 0; i < records.value.length; i++) {
if (i < 4) { recArr.push(records.value[i]) }
}
records.value = recArr
uni.showToast({ title: '识别成功', icon: 'success' })
}
function onScan(res : any) : void {
const arr = res as string[]
if (arr == null || arr.length == 0) { return }
handleCode(arr[0])
}
function feedbackClass(theme : string) : string {
return `sc-st sc-st-${theme}`
}
function feedbackColor(theme : string) : string {
if (theme == 'ok') { return '#10B981' }
if (theme == 'warn') { return '#F59E0B' }
return '#DC2626'
}
function onRecord(r : HrScanRecord) : void {
uni.showModal({ title: r.name, content: r.time + ' · ' + r.delta + '\n状态:' + r.status, showCancel: false })
}
function toggleFlash() : void {
flash.value = !flash.value
uni.showToast({ title: flash.value ? '已打开手电' : '已关闭手电', icon: 'none' })
}
/*
* 取景框扫描线
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
* 改为定时器逐帧推进 topsc-frame 高 372rpx16%~84% ⇒ 60~313rpx。
* 周期 2.2s68ms × 32 帧),与设计稿 hrScanMove 一致。
*/
const laserTop = ref(60)
let laserT = 0.0
let laserTimer : number = 0
const laserStyle = computed(() : string => {
return 'top:' + laserTop.value.toString() + 'rpx;'
})
function startLaser() : void {
if (laserTimer != 0) { return }
laserTimer = setInterval(() => {
laserT = laserT + 0.03125 // 1/3232 帧正好走完一个往返,端点(16%/84%)可精确命中
if (laserT > 1) { laserT = laserT - 1 }
const k = laserT < 0.5 ? laserT * 2 : (1 - laserT) * 2
laserTop.value = Math.round(60 + k * 253)
}, 68)
}
function stopLaser() : void {
if (laserTimer != 0) {
clearInterval(laserTimer)
laserTimer = 0
}
}
onLoad(() => {
startLaser()
})
onUnload(() => {
stopLaser()
})
</script>
<style>
.wk-nav {
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #FFFFFF;
border-bottom: 2rpx solid #F3F4F6;
}
.wk-nav-t {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.wk-nav-a {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
}
.wk-nav-a-t {
font-size: 29.2rpx;
color: #6B7280;
margin-left: 8rpx;
}
/* ---------- 取景框 ---------- */
.sc-box {
width: 100%;
height: 686rpx;
background-color: #0F172A;
border-radius: 32rpx;
overflow: hidden;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sc-frame {
width: 372rpx;
height: 372rpx;
position: relative;
}
.sc-cf {
position: absolute;
width: 52rpx;
height: 52rpx;
border: 6rpx solid #10B981;
border-radius: 8rpx;
}
.sc-cf-tl {
top: 0rpx;
left: 0rpx;
border-right-width: 0rpx;
border-bottom-width: 0rpx;
}
.sc-cf-tr {
top: 0rpx;
right: 0rpx;
border-left-width: 0rpx;
border-bottom-width: 0rpx;
}
.sc-cf-bl {
bottom: 0rpx;
left: 0rpx;
border-right-width: 0rpx;
border-top-width: 0rpx;
}
.sc-cf-br {
bottom: 0rpx;
right: 0rpx;
border-left-width: 0rpx;
border-top-width: 0rpx;
}
/* 扫描线:⚠ App 端 uvue 不支持 @keyframes/animation
上下位移已改为 script 内定时器推进 laserTop + laserStyle 下发 top。 */
.sc-line {
position: absolute;
left: 9%;
right: 9%;
top: 60rpx;
height: 4rpx;
background-color: #10B981;
}
.sc-tip {
position: absolute;
bottom: 24rpx;
left: 0rpx;
right: 0rpx;
text-align: center;
font-size: 25.8rpx;
color: rgba(255, 255, 255, 0.78);
}
.sc-seg-t {
font-size: 29.2rpx;
color: #6B7280;
}
.sc-seg-t-on {
color: #2563EB;
font-weight: bold;
}
/* ---------- 三态反馈 ---------- */
.sc-st {
display: flex;
flex-direction: row;
align-items: center;
padding: 22rpx 24rpx 22rpx 24rpx;
border-radius: 22rpx;
margin-bottom: 16rpx;
}
.sc-st-ok {
background-color: #ECFDF5;
border: 2rpx solid #A7F3D0;
}
.sc-st-warn {
background-color: #FFFBEB;
border: 2rpx solid #FDE68A;
}
.sc-st-err {
background-color: #FEF2F2;
border: 2rpx solid #FECACA;
}
.sc-st-ic {
width: 52rpx;
height: 52rpx;
border-radius: 16rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.sc-st-ic-t {
font-size: 29.2rpx;
font-weight: bold;
color: #FFFFFF;
}
.sc-st-b {
font-size: 28rpx;
font-weight: bold;
}
.sc-st-s {
font-size: 23.6rpx;
margin-top: 4rpx;
}
.sc-empty {
align-items: center;
padding-top: 40rpx;
padding-bottom: 40rpx;
}
.sc-empty-t {
font-size: 28rpx;
font-weight: bold;
color: #6B7280;
margin-top: 16rpx;
}
.sc-empty-s {
font-size: 23.6rpx;
color: #9CA3AF;
margin-top: 8rpx;
text-align: center;
}
</style>
+206
View File
@@ -0,0 +1,206 @@
<template>
<view class="hr-page" style="background-color:#F3F4F6">
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
<view class="wk-nav">
<text class="wk-nav-t">库存查询</text>
<view class="wk-nav-a" @click="onExport">
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="16"></x-icon>
<text class="wk-nav-a-t">导出</text>
</view>
</view>
<view class="hr-flow">
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
<view class="hr-body">
<!-- 搜索(xSearch -->
<x-search v-model="keyword" placeholder="搜索物资名称 / 编码" round="24rpx" height="84rpx"
:border="searchBorder" input-bg-color="#FFFFFF" icon-color="#9CA3AF" font-color="#1F2937"
:show-cancel="false"></x-search>
<view class="st-bar">
<view class="st-cell">
<text class="st-b">128</text>
<text class="st-s">物资品类</text>
</view>
<view class="st-sep"></view>
<view class="st-cell">
<text class="st-b" style="color:#DC2626">6</text>
<text class="st-s">低库存预警</text>
</view>
<view class="st-sep"></view>
<view class="st-cell">
<text class="st-b" style="color:#2563EB">14</text>
<text class="st-s">借用中</text>
</view>
</view>
<!-- ============ 库存预警 ============ -->
<hr-sec title="库存预警" :more="warnList.length + ' 项 '"></hr-sec>
<hr-card variant="danger">
<hr-lrow v-for="(w, i) in warnList" :key="i" :icon="w.icon" :theme="w.low ? 'red' : 'amb'"
:title="w.name" :sub="w.spec" :value="w.value" :value-theme="w.low ? 'red' : 'amb'"
:value-bold="true" :is-last="i == warnList.length - 1" @click="openGoods(w)"></hr-lrow>
</hr-card>
<!-- ============ 全部物资 ============ -->
<hr-sec title="全部物资" more="按库存排序 "></hr-sec>
<hr-card variant="list">
<hr-lrow v-for="(s, i) in shownAll" :key="i" :icon="s.icon"
:theme="s.low ? 'amb' : 'pri'" :title="s.name" :sub="s.spec" :value="s.value"
:value-theme="s.valueTheme" :value-bold="true" :value-size="'26rpx'"
:is-last="i == shownAll.length - 1" @click="openGoods(s)"></hr-lrow>
</hr-card>
<hr-empty v-if="shownAll.length == 0" icon="⌕" title="没有匹配的物资"
sub="换个物资名称或编码试试"></hr-empty>
<view style="height:28rpx"></view>
</view>
</scroll-view>
<!-- 加载中覆盖层(组件库 xLoading -->
<view v-if="loading" class="hr-loading-mask">
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
text-color="#6B7280"></x-loading>
</view>
</view>
<hr-tabbar :active="1"></hr-tabbar>
</view>
</template>
<script lang="uts" setup>
import { ref, computed } from 'vue'
import { stockWarn, stockAll } from '@/mock/index.uts'
import { HrStockItem } from '@/mock/types.uts'
import { navTo } from '@/utils/nav.uts'
import { mockRequest } from '@/utils/request.uts'
/**
* s24 库存查询(仓管 Tab「仓库」)
* 库存预警自动置顶,支持关键字过滤。
*/
const loading = ref(true)
const keyword = ref('')
/**
* xSearch 的 border 是「数组」prop,固定 3 项 = [宽度, 线型, 颜色],
* 少于 3 项内部直接返回 none。设计稿搜索框为 1px solid #E5E7EB。
*/
const searchBorder : string[] = ['1px', 'solid', '#E5E7EB']
onLoad(() => {
mockRequest(() => {
loading.value = false
})
})
const warnList = ref<HrStockItem[]>(stockWarn)
const allList = ref<HrStockItem[]>(stockAll)
const shownAll = computed(() : HrStockItem[] => {
if (keyword.value == '') { return allList.value }
const kw = keyword.value.toLowerCase()
const arr : HrStockItem[] = []
for (let i = 0; i < allList.value.length; i++) {
if (allList.value[i].name.toLowerCase().indexOf(kw) > -1) { arr.push(allList.value[i]) }
}
return arr
})
function openGoods(s : HrStockItem) : void {
navTo('/pages/warehouse/goods-detail?name=' + s.name)
}
function onExport() : void {
uni.showToast({ title: '已生成库存清单 Excel', icon: 'none' })
}
</script>
<style>
.wk-nav {
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 32rpx;
padding-right: 32rpx;
background-color: #FFFFFF;
border-bottom: 2rpx solid #F3F4F6;
}
.wk-nav-t {
font-size: 33.6rpx;
font-weight: bold;
color: #1F2937;
}
.wk-nav-a {
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
}
.wk-nav-a-t {
font-size: 29.2rpx;
color: #6B7280;
margin-left: 8rpx;
}
.st-srch {
display: flex;
flex-direction: row;
align-items: center;
background-color: #FFFFFF;
border: 2rpx solid #E5E7EB;
border-radius: 22rpx;
height: 92rpx;
padding: 0rpx 24rpx 0rpx 24rpx;
}
.st-srch-input {
flex: 1;
font-size: 29.2rpx;
color: #1F2937;
height: 88rpx;
padding: 0rpx 18rpx 0rpx 18rpx;
}
.st-bar {
display: flex;
flex-direction: row;
background-color: #FFFFFF;
border: 2rpx solid #E5E7EB;
border-radius: 26rpx;
padding: 26rpx 8rpx 26rpx 8rpx;
margin-top: 20rpx;
}
.st-cell {
flex: 1;
align-items: center;
}
.st-b {
font-size: 44.8rpx;
font-weight: bold;
color: #1F2937;
}
.st-s {
font-size: 23.6rpx;
color: #6B7280;
margin-top: 6rpx;
}
.st-sep {
width: 2rpx;
height: 60rpx;
background-color: #F3F4F6;
margin-top: 6rpx;
}
</style>