|
|
@@ -1,67 +1,99 @@
|
|
|
<template>
|
|
|
<view class="page">
|
|
|
- <NavBar title="补货首页" />
|
|
|
+ <!-- 黄色导航栏 -->
|
|
|
+ <view class="yellow-header">
|
|
|
+ <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
|
|
+ <view class="header-content">
|
|
|
+ <text class="header-title">补货管理</text>
|
|
|
+ <view class="header-actions">
|
|
|
+ <view class="action-dot"></view>
|
|
|
+ <view class="action-dot"></view>
|
|
|
+ <view class="action-dot"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
|
|
|
|
|
- <!-- 补货员信息 -->
|
|
|
- <view class="user-card">
|
|
|
- <view class="user-header">
|
|
|
- <view class="avatar">
|
|
|
- <text class="avatar-text">{{ (replenisherInfo.name || '补').charAt(0) }}</text>
|
|
|
+ <!-- 预警模式 -->
|
|
|
+ <view class="settings-card">
|
|
|
+ <view class="setting-row">
|
|
|
+ <view class="setting-label">
|
|
|
+ <text class="label-icon warning-icon">!</text>
|
|
|
+ <text class="label-text">预警模式</text>
|
|
|
+ <text class="label-tip">i</text>
|
|
|
</view>
|
|
|
- <view class="user-info">
|
|
|
- <text class="user-name">{{ replenisherInfo.name || '补货员' }}</text>
|
|
|
- <text class="user-role">补货员</text>
|
|
|
+ </view>
|
|
|
+ <view class="radio-row">
|
|
|
+ <view class="radio-item" :class="{ active: alertMode === 'standard' }" @click="alertMode = 'standard'">
|
|
|
+ <view class="radio-dot" :class="{ checked: alertMode === 'standard' }"></view>
|
|
|
+ <text>根据标准库存预警</text>
|
|
|
</view>
|
|
|
- <view class="user-meta" v-if="replenisherInfo.employeeId">
|
|
|
- <text class="meta-label">工号</text>
|
|
|
- <text class="meta-value">{{ replenisherInfo.employeeId }}</text>
|
|
|
+ <view class="radio-item" :class="{ active: alertMode === 'sales' }" @click="alertMode = 'sales'">
|
|
|
+ <view class="radio-dot" :class="{ checked: alertMode === 'sales' }"></view>
|
|
|
+ <text>根据销量预警</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <view class="user-stats">
|
|
|
- <view class="stat-item">
|
|
|
- <text class="stat-value">{{ deviceList.length }}</text>
|
|
|
- <text class="stat-label">绑定设备</text>
|
|
|
+
|
|
|
+ <view class="setting-row threshold-row">
|
|
|
+ <view class="setting-label">
|
|
|
+ <text class="label-icon threshold-icon">%</text>
|
|
|
+ <text class="label-text">报警阈值(%)</text>
|
|
|
+ <text class="label-tip">i</text>
|
|
|
+ </view>
|
|
|
+ <view class="threshold-value" @click="editThreshold">
|
|
|
+ <text>{{ alertThreshold }}%</text>
|
|
|
+ <text class="edit-icon">✎</text>
|
|
|
</view>
|
|
|
- <view class="stat-item">
|
|
|
- <text class="stat-value">{{ pendingOrderTotal }}</text>
|
|
|
- <text class="stat-label">待处理补货单</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="setting-row">
|
|
|
+ <view class="setting-label">
|
|
|
+ <text class="label-icon camera-icon">📷</text>
|
|
|
+ <text class="label-text">补货员上货后必须拍照</text>
|
|
|
</view>
|
|
|
- <view class="stat-item">
|
|
|
- <text class="stat-value">{{ lowStockTotal }}</text>
|
|
|
- <text class="stat-label">待补货商品</text>
|
|
|
+ <view class="toggle-switch" :class="{ on: requirePhoto }" @click="requirePhoto = !requirePhoto">
|
|
|
+ <view class="toggle-knob"></view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 快捷入口 -->
|
|
|
- <view class="quick-row">
|
|
|
- <view class="quick-btn orders" @click="goToOrders">
|
|
|
- <view class="quick-icon doc-icon"></view>
|
|
|
- <text class="quick-label">补货单</text>
|
|
|
+ <!-- 统计行 -->
|
|
|
+ <view class="stats-row">
|
|
|
+ <view class="stat-item" @click="activeFilter = 'all'">
|
|
|
+ <text class="stat-num" :class="{ highlight: activeFilter === 'all' }">{{ stats.totalDevices }}</text>
|
|
|
+ <text class="stat-label" :class="{ highlight: activeFilter === 'all' }">全部设备</text>
|
|
|
</view>
|
|
|
- <view class="quick-btn scan" @click="handleScanDevice">
|
|
|
- <view class="quick-icon scan-icon-css"></view>
|
|
|
- <text class="quick-label">扫码开柜</text>
|
|
|
+ <view class="stat-item" @click="activeFilter = 'pending-order'">
|
|
|
+ <text class="stat-num" :class="{ highlight: activeFilter === 'pending-order' }">{{ stats.pendingOrderDevices }}</text>
|
|
|
+ <text class="stat-label" :class="{ highlight: activeFilter === 'pending-order' }">待出补货单设备</text>
|
|
|
</view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- 筛选标签 -->
|
|
|
- <view class="filter-tabs">
|
|
|
- <view class="filter-tab" :class="{ active: activeFilter === 'all' }" @click="activeFilter = 'all'">
|
|
|
- <text>全部</text>
|
|
|
- <text class="tab-count">{{ deviceList.length }}</text>
|
|
|
+ <view class="stat-item" @click="activeFilter = 'pending-task'">
|
|
|
+ <text class="stat-num" :class="{ highlight: activeFilter === 'pending-task' }">{{ stats.pendingTasks }}</text>
|
|
|
+ <text class="stat-label" :class="{ highlight: activeFilter === 'pending-task' }">待完成补货单</text>
|
|
|
</view>
|
|
|
- <view class="filter-tab" :class="{ active: activeFilter === 'pending' }" @click="activeFilter = 'pending'">
|
|
|
- <text>有补货单</text>
|
|
|
- <text class="tab-count">{{ pendingDeviceCount }}</text>
|
|
|
+ <view class="stat-item" @click="activeFilter = 'today'">
|
|
|
+ <text class="stat-num" :class="{ highlight: activeFilter === 'today' }">{{ stats.todayReplenished }}</text>
|
|
|
+ <text class="stat-label" :class="{ highlight: activeFilter === 'today' }">今日已补货(补货记录)</text>
|
|
|
</view>
|
|
|
- <view class="filter-tab" :class="{ active: activeFilter === 'submitted' }" @click="activeFilter = 'submitted'">
|
|
|
- <text>待完成</text>
|
|
|
- <text class="tab-count">{{ submittedDeviceCount }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 搜索与筛选 -->
|
|
|
+ <view class="search-bar">
|
|
|
+ <view class="search-input-wrap">
|
|
|
+ <text class="search-icon">🔍</text>
|
|
|
+ <input
|
|
|
+ class="search-input"
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="请输入设备编号/名称"
|
|
|
+ placeholder-class="search-placeholder"
|
|
|
+ confirm-type="search"
|
|
|
+ @confirm="handleSearch"
|
|
|
+ />
|
|
|
</view>
|
|
|
- <view class="filter-tab" :class="{ active: activeFilter === 'today' }" @click="activeFilter = 'today'">
|
|
|
- <text>今日已补</text>
|
|
|
- <text class="tab-count">{{ todayDeviceCount }}</text>
|
|
|
+ <view class="search-btn" @click="handleSearch">搜索</view>
|
|
|
+ <view class="filter-btn" @click="showFilter = !showFilter">
|
|
|
+ <text>筛选</text>
|
|
|
+ <text class="filter-icon">▽</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -72,187 +104,351 @@
|
|
|
|
|
|
<!-- 设备列表 -->
|
|
|
<view class="device-section" v-else>
|
|
|
- <view class="section-header">
|
|
|
- <text class="section-title">我的设备</text>
|
|
|
- <text class="section-count" v-if="filteredDevices.length">共 {{ filteredDevices.length }} 台</text>
|
|
|
- </view>
|
|
|
-
|
|
|
<view class="empty-state" v-if="filteredDevices.length === 0">
|
|
|
- <view class="empty-icon"><view class="icon-device-empty"></view></view>
|
|
|
<text class="empty-text">{{ activeFilter === 'all' ? '暂无绑定的设备' : '没有匹配的设备' }}</text>
|
|
|
<text class="empty-hint">{{ activeFilter === 'all' ? '请联系管理员绑定设备后开始补货' : '当前筛选条件下没有设备' }}</text>
|
|
|
</view>
|
|
|
|
|
|
- <view class="device-card" v-for="(device, index) in filteredDevices" :key="index" @click="goToOperation(device)">
|
|
|
- <view class="card-head">
|
|
|
- <view class="device-info">
|
|
|
- <text class="device-name">{{ device.name || device.deviceId }}</text>
|
|
|
- <text class="device-sn">{{ device.deviceId }}</text>
|
|
|
+ <view
|
|
|
+ class="device-card"
|
|
|
+ v-for="(device, index) in filteredDevices"
|
|
|
+ :key="device.deviceId || index"
|
|
|
+ @click="goToOperation(device)"
|
|
|
+ >
|
|
|
+ <view class="card-top">
|
|
|
+ <text class="device-name">{{ device.name || device.deviceId }}</text>
|
|
|
+ <view class="order-tag" v-if="device.replenishOrderGenerated">补货单已生成</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="card-body">
|
|
|
+ <view class="inventory-badge" :class="getInventoryLevel(device.inventoryPercent)">
|
|
|
+ <text class="badge-label">库存</text>
|
|
|
+ <text class="badge-value">{{ device.inventoryPercent || 0 }}%</text>
|
|
|
</view>
|
|
|
- <view class="status-tag" :class="device.status === 1 ? 'online' : 'offline'">
|
|
|
- <view class="status-dot"></view>
|
|
|
- <text>{{ device.statusLabel || (device.status === 1 ? '在线' : '离线') }}</text>
|
|
|
+ <view class="metrics-row">
|
|
|
+ <view class="metric-item">
|
|
|
+ <text class="metric-value">{{ device.standardStock || 0 }}</text>
|
|
|
+ <text class="metric-label">标准库存</text>
|
|
|
+ </view>
|
|
|
+ <view class="metric-item">
|
|
|
+ <text class="metric-value">{{ device.remainingStock || 0 }}</text>
|
|
|
+ <text class="metric-label">剩余库存</text>
|
|
|
+ </view>
|
|
|
+ <view class="metric-item">
|
|
|
+ <text class="metric-value">{{ device.dailySales || 0 }}</text>
|
|
|
+ <text class="metric-label">日均销量</text>
|
|
|
+ <text class="info-tip">i</text>
|
|
|
+ </view>
|
|
|
+ <view class="metric-item">
|
|
|
+ <text class="metric-value">{{ device.salesRatio || 0 }}</text>
|
|
|
+ <text class="metric-label">销容比</text>
|
|
|
+ <text class="info-tip">i</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
+ <text class="card-arrow">></text>
|
|
|
</view>
|
|
|
|
|
|
- <view class="device-addr" v-if="device.address">
|
|
|
- <text>{{ device.address }}</text>
|
|
|
+ <!-- 勾选框 -->
|
|
|
+ <view class="card-checkbox" :class="{ checked: device.selected }" @click.stop="toggleSelect(device)">
|
|
|
+ <text v-if="device.selected" class="check-mark">✓</text>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
|
|
|
- <view class="stock-row">
|
|
|
- <view class="stock-chip" v-if="device.pendingOrderCount > 0">
|
|
|
- <text class="chip-label">待补货单</text>
|
|
|
- <text class="chip-value accent">{{ device.pendingOrderCount }}</text>
|
|
|
- </view>
|
|
|
- <view class="stock-chip">
|
|
|
- <text class="chip-label">种类</text>
|
|
|
- <text class="chip-value">{{ device.productCount || 0 }}</text>
|
|
|
- </view>
|
|
|
- <view class="stock-chip">
|
|
|
- <text class="chip-label">总库存</text>
|
|
|
- <text class="chip-value">{{ device.totalStock || 0 }}</text>
|
|
|
- </view>
|
|
|
- <view class="stock-chip warn" v-if="device.lowStockCount > 0">
|
|
|
- <text class="chip-label">低库存</text>
|
|
|
- <text class="chip-value">{{ device.lowStockCount }}</text>
|
|
|
- </view>
|
|
|
- <view class="stock-chip danger" v-if="device.zeroStockCount > 0">
|
|
|
- <text class="chip-label">缺货</text>
|
|
|
- <text class="chip-value">{{ device.zeroStockCount }}</text>
|
|
|
+ <!-- 底部批量操作栏 -->
|
|
|
+ <view class="bottom-bar" v-if="filteredDevices.length > 0">
|
|
|
+ <view class="bottom-left">
|
|
|
+ <view class="select-all-btn" @click="toggleSelectAll">
|
|
|
+ <view class="checkbox-icon" :class="{ checked: allSelected }">
|
|
|
+ <text v-if="allSelected" class="check-mark">✓</text>
|
|
|
</view>
|
|
|
+ <text>全选</text>
|
|
|
</view>
|
|
|
+ <text class="selected-count">已选({{ selectedCount }})</text>
|
|
|
+ </view>
|
|
|
+ <view class="batch-btn" @click="handleBatchGenerate">
|
|
|
+ <text>批量生成补货单</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
|
|
|
- <view class="card-foot">
|
|
|
- <text class="foot-text">查看商品库存</text>
|
|
|
- <text class="foot-arrow">></text>
|
|
|
+ <!-- 阈值编辑弹窗 -->
|
|
|
+ <view class="modal-mask" v-if="showThresholdModal" @click="showThresholdModal = false">
|
|
|
+ <view class="modal-content" @click.stop>
|
|
|
+ <text class="modal-title">设置报警阈值</text>
|
|
|
+ <view class="modal-input-wrap">
|
|
|
+ <input
|
|
|
+ class="modal-input"
|
|
|
+ type="number"
|
|
|
+ v-model="tempThreshold"
|
|
|
+ placeholder="请输入百分比"
|
|
|
+ />
|
|
|
+ <text class="modal-suffix">%</text>
|
|
|
+ </view>
|
|
|
+ <view class="modal-actions">
|
|
|
+ <view class="modal-btn cancel" @click="showThresholdModal = false">取消</view>
|
|
|
+ <view class="modal-btn confirm" @click="confirmThreshold">确定</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <CustomTabBar />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed } from 'vue';
|
|
|
+import { ref, computed, onMounted } from 'vue';
|
|
|
import { onShow } from '@dcloudio/uni-app';
|
|
|
-import NavBar from '@/components/NavBar.vue';
|
|
|
-import { getReplenisherInfo } from '@/utils/auth';
|
|
|
-import { getMyInfo, getDeviceList } from '@/api/replenish';
|
|
|
+import CustomTabBar from '@/components/CustomTabBar.vue';
|
|
|
+import { getDeviceList, batchGenerateOrders } from '@/api/replenish';
|
|
|
+
|
|
|
+const statusBarHeight = ref(0);
|
|
|
+
|
|
|
+// 设置
|
|
|
+const alertMode = ref<'standard' | 'sales'>('standard');
|
|
|
+const alertThreshold = ref(80);
|
|
|
+const requirePhoto = ref(false);
|
|
|
+const showThresholdModal = ref(false);
|
|
|
+const tempThreshold = ref(80);
|
|
|
+
|
|
|
+// 搜索筛选
|
|
|
+const searchKeyword = ref('');
|
|
|
+const showFilter = ref(false);
|
|
|
+const activeFilter = ref('all');
|
|
|
|
|
|
-const replenisherInfo = ref<any>({});
|
|
|
+// 数据
|
|
|
const deviceList = ref<any[]>([]);
|
|
|
const loading = ref(true);
|
|
|
-const activeFilter = ref('all');
|
|
|
-const pendingOrderTotal = ref(0);
|
|
|
-const lowStockTotal = ref(0);
|
|
|
+
|
|
|
+// 统计
|
|
|
+const stats = ref({
|
|
|
+ totalDevices: 0,
|
|
|
+ pendingOrderDevices: 0,
|
|
|
+ pendingTasks: 0,
|
|
|
+ todayReplenished: 0
|
|
|
+});
|
|
|
|
|
|
const filteredDevices = computed(() => {
|
|
|
+ let list = deviceList.value;
|
|
|
+ // 关键词筛选
|
|
|
+ if (searchKeyword.value) {
|
|
|
+ const kw = searchKeyword.value.toLowerCase();
|
|
|
+ list = list.filter(d =>
|
|
|
+ (d.deviceId || '').toLowerCase().includes(kw) ||
|
|
|
+ (d.name || '').toLowerCase().includes(kw)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ // 标签筛选
|
|
|
switch (activeFilter.value) {
|
|
|
- case 'pending': return deviceList.value.filter(d => (d.pendingOrderCount || 0) > 0);
|
|
|
- case 'submitted': return deviceList.value.filter(d => (d.submittedOrderCount || 0) > 0);
|
|
|
- case 'today': return deviceList.value.filter(d => d.todayReplenished);
|
|
|
- default: return deviceList.value;
|
|
|
+ case 'pending-order':
|
|
|
+ return list.filter(d => !d.replenishOrderGenerated);
|
|
|
+ case 'pending-task':
|
|
|
+ return list.filter(d => d.replenishOrderGenerated && !d.todayReplenished);
|
|
|
+ case 'today':
|
|
|
+ return list.filter(d => d.todayReplenished);
|
|
|
+ default:
|
|
|
+ return list;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
-const pendingDeviceCount = computed(() => deviceList.value.filter(d => (d.pendingOrderCount || 0) > 0).length);
|
|
|
-const submittedDeviceCount = computed(() => deviceList.value.filter(d => (d.submittedOrderCount || 0) > 0).length);
|
|
|
-const todayDeviceCount = computed(() => deviceList.value.filter(d => d.todayReplenished).length);
|
|
|
+const selectedCount = computed(() => deviceList.value.filter(d => d.selected).length);
|
|
|
+const allSelected = computed(() => filteredDevices.value.length > 0 && filteredDevices.value.every(d => d.selected));
|
|
|
|
|
|
-const goToOrders = () => uni.navigateTo({ url: '/pages/replenish/orders' });
|
|
|
+const getInventoryLevel = (percent: number): string => {
|
|
|
+ if (percent <= 30) return 'danger';
|
|
|
+ if (percent <= 60) return 'warning';
|
|
|
+ return 'normal';
|
|
|
+};
|
|
|
+
|
|
|
+const editThreshold = () => {
|
|
|
+ tempThreshold.value = alertThreshold.value;
|
|
|
+ showThresholdModal.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const confirmThreshold = () => {
|
|
|
+ const val = parseInt(tempThreshold.value);
|
|
|
+ if (val >= 0 && val <= 100) {
|
|
|
+ alertThreshold.value = val;
|
|
|
+ }
|
|
|
+ showThresholdModal.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+const toggleSelect = (device: any) => {
|
|
|
+ device.selected = !device.selected;
|
|
|
+};
|
|
|
+
|
|
|
+const toggleSelectAll = () => {
|
|
|
+ const shouldSelect = !allSelected.value;
|
|
|
+ filteredDevices.value.forEach(d => { d.selected = shouldSelect; });
|
|
|
+};
|
|
|
+
|
|
|
+const handleSearch = () => {
|
|
|
+ // 触发筛选
|
|
|
+};
|
|
|
|
|
|
const goToOperation = (device: any) => {
|
|
|
uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${device.deviceId}` });
|
|
|
};
|
|
|
|
|
|
-const handleScanDevice = () => {
|
|
|
- uni.scanCode({
|
|
|
- onlyFromCamera: false,
|
|
|
- scanType: ['qrCode'],
|
|
|
- success: (res: any) => {
|
|
|
- const match = (res.result || '').match(/\/([^/]+)$/);
|
|
|
- if (!match) { uni.showToast({ title: '无效的设备二维码', icon: 'none' }); return; }
|
|
|
- const id = match[1];
|
|
|
- if (!deviceList.value.find((d: any) => d.deviceId === id)) {
|
|
|
- uni.showToast({ title: '该设备未绑定,请联系管理员', icon: 'none' });
|
|
|
- return;
|
|
|
- }
|
|
|
- uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${id}` });
|
|
|
- }
|
|
|
- });
|
|
|
+const handleBatchGenerate = async () => {
|
|
|
+ const selected = deviceList.value.filter(d => d.selected);
|
|
|
+ if (selected.length === 0) {
|
|
|
+ uni.showToast({ title: '请先选择设备', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const ids = selected.map(d => d.deviceId);
|
|
|
+ await batchGenerateOrders({ deviceIds: ids });
|
|
|
+ uni.showToast({ title: `已为${selected.length}台设备生成补货单`, icon: 'success' });
|
|
|
+ // 刷新数据
|
|
|
+ await loadData();
|
|
|
+ } catch (e: any) {
|
|
|
+ uni.showToast({ title: e.message || '生成失败', icon: 'none' });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-onShow(async () => {
|
|
|
+const loadData = async () => {
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
- const info = getReplenisherInfo();
|
|
|
- if (info?.id) replenisherInfo.value = info;
|
|
|
- const detail = await getMyInfo();
|
|
|
- if (detail) replenisherInfo.value = detail;
|
|
|
-
|
|
|
const devices = await getDeviceList();
|
|
|
- deviceList.value = devices || [];
|
|
|
- lowStockTotal.value = (devices || []).reduce((s: number, d: any) => s + (d.lowStockCount || 0) + (d.zeroStockCount || 0), 0);
|
|
|
- pendingOrderTotal.value = (devices || []).reduce((s: number, d: any) => s + (d.pendingOrderCount || 0), 0);
|
|
|
+ deviceList.value = (devices || []).map(d => ({ ...d, selected: false }));
|
|
|
+ stats.value = {
|
|
|
+ totalDevices: deviceList.value.length,
|
|
|
+ pendingOrderDevices: deviceList.value.filter(d => !d.replenishOrderGenerated).length,
|
|
|
+ pendingTasks: deviceList.value.filter(d => d.replenishOrderGenerated && !d.todayReplenished).length,
|
|
|
+ todayReplenished: deviceList.value.filter(d => d.todayReplenished).length
|
|
|
+ };
|
|
|
} catch (e: any) {
|
|
|
uni.showToast({ title: e.message || '加载失败', icon: 'none' });
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
+ statusBarHeight.value = systemInfo.statusBarHeight || 20;
|
|
|
+});
|
|
|
+
|
|
|
+onShow(async () => {
|
|
|
+ await loadData();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.page { min-height: 100vh; background: $bg-color-page; padding-bottom: 48rpx; }
|
|
|
+.page { min-height: 100vh; background: $bg-color-page; padding-bottom: 160rpx; }
|
|
|
+
|
|
|
+// ====== 黄色导航栏 ======
|
|
|
+.yellow-header {
|
|
|
+ position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
|
|
+ background: $primary-color;
|
|
|
+ padding-top: constant(safe-area-inset-top);
|
|
|
+ padding-top: env(safe-area-inset-top);
|
|
|
+}
|
|
|
+.header-content {
|
|
|
+ display: flex; align-items: center; justify-content: space-between;
|
|
|
+ height: 44px; padding: 0 16rpx;
|
|
|
+}
|
|
|
+.header-title { font-size: 34rpx; font-weight: 700; color: #1e293b; }
|
|
|
+.header-actions { display: flex; gap: 6rpx; padding: 8rpx;
|
|
|
+ .action-dot { width: 6rpx; height: 6rpx; border-radius: 50%; background: #1e293b; }
|
|
|
+}
|
|
|
+.header-placeholder { width: 100%; }
|
|
|
|
|
|
-// ====== User card ======
|
|
|
-.user-card { margin: 16rpx 24rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); }
|
|
|
-.user-header { display: flex; align-items: center; margin-bottom: $spacing-3;
|
|
|
- .avatar { width: 72rpx; height: 72rpx; background: $primary-color-bg; border-radius: $radius-lg; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0;
|
|
|
- .avatar-text { font-size: 32rpx; font-weight: 700; color: $primary-color-dark; }
|
|
|
+// ====== 设置卡片 ======
|
|
|
+.settings-card {
|
|
|
+ margin: 16rpx 24rpx; padding: 24rpx; background: $bg-color-card;
|
|
|
+ border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+.setting-row {
|
|
|
+ display: flex; align-items: center; justify-content: space-between;
|
|
|
+ padding: 12rpx 0;
|
|
|
+}
|
|
|
+.setting-label {
|
|
|
+ display: flex; align-items: center; gap: 8rpx;
|
|
|
+ .label-icon {
|
|
|
+ width: 32rpx; height: 32rpx; border-radius: 50%;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 18rpx; font-weight: 700; color: #fff;
|
|
|
+ }
|
|
|
+ .warning-icon { background: $warning-color; }
|
|
|
+ .threshold-icon { background: $primary-color; font-size: 16rpx; }
|
|
|
+ .camera-icon { background: $primary-color; font-size: 16rpx; }
|
|
|
+ .label-text { font-size: $font-size-base; font-weight: 600; color: $text-color-primary; }
|
|
|
+ .label-tip {
|
|
|
+ width: 28rpx; height: 28rpx; border-radius: 50%;
|
|
|
+ background: $warning-color-bg; color: $warning-color;
|
|
|
+ font-size: 16rpx; font-weight: 700; font-style: italic;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
}
|
|
|
- .user-info { flex: 1;
|
|
|
- .user-name { display: block; font-size: $font-size-lg; font-weight: 600; color: $text-color-primary; }
|
|
|
- .user-role { font-size: $font-size-sm; color: $primary-color; font-weight: 500; }
|
|
|
+}
|
|
|
+.radio-row { display: flex; gap: 32rpx; padding: 8rpx 0 16rpx;
|
|
|
+ .radio-item { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-sm; color: $text-color-secondary;
|
|
|
+ .radio-dot { width: 28rpx; height: 28rpx; border-radius: 50%; border: 3rpx solid $border-color;
|
|
|
+ &.checked { border-color: $primary-color; background: $primary-color;
|
|
|
+ &::after { content: ''; display: block; width: 10rpx; height: 10rpx; border-radius: 50%; background: #fff; margin: 6rpx auto; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.active { color: $text-color-primary; font-weight: 500; }
|
|
|
}
|
|
|
- .user-meta { text-align: right;
|
|
|
- .meta-label { display: block; font-size: 20rpx; color: $text-color-muted; }
|
|
|
- .meta-value { font-size: 22rpx; color: $text-color-secondary; }
|
|
|
+}
|
|
|
+.threshold-row { border-top: 1rpx solid $border-color-light;
|
|
|
+ .threshold-value { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-lg; font-weight: 700; color: $text-color-primary;
|
|
|
+ .edit-icon { font-size: 24rpx; color: $primary-color; }
|
|
|
}
|
|
|
}
|
|
|
-.user-stats { display: flex; background: $bg-color-page; border-radius: $radius-sm; padding: 16rpx 0;
|
|
|
- .stat-item { flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
|
- .stat-value { font-size: 30rpx; font-weight: 700; color: $text-color-primary; }
|
|
|
- .stat-label { font-size: 22rpx; color: $text-color-muted; margin-top: 4rpx; }
|
|
|
+.toggle-switch {
|
|
|
+ width: 80rpx; height: 44rpx; border-radius: 22rpx;
|
|
|
+ background: $border-color; position: relative; transition: background 0.3s;
|
|
|
+ .toggle-knob {
|
|
|
+ width: 38rpx; height: 38rpx; border-radius: 50%; background: #fff;
|
|
|
+ position: absolute; top: 3rpx; left: 3rpx; transition: left 0.3s;
|
|
|
+ box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.15);
|
|
|
+ }
|
|
|
+ &.on { background: $primary-color;
|
|
|
+ .toggle-knob { left: 39rpx; }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// ====== Filter tabs ======
|
|
|
-.filter-tabs { display: flex; gap: 8rpx; margin: 0 24rpx 16rpx; }
|
|
|
-.filter-tab { display: flex; align-items: center; gap: 6rpx; padding: 12rpx 20rpx; background: $bg-color-card; border-radius: $radius-full; font-size: 24rpx; color: $text-color-secondary; border: 1rpx solid transparent;
|
|
|
- .tab-count { font-size: 20rpx; color: $text-color-muted; }
|
|
|
- &.active { background: $primary-color-bg; border-color: $primary-color; color: $primary-color-dark; font-weight: 500;
|
|
|
- .tab-count { color: $primary-color; }
|
|
|
+// ====== 统计行 ======
|
|
|
+.stats-row {
|
|
|
+ display: flex; padding: 16rpx 24rpx; gap: 0;
|
|
|
+ .stat-item { flex: 1; text-align: center; padding: 12rpx 0;
|
|
|
+ .stat-num { display: block; font-size: 40rpx; font-weight: 800; color: $text-color-primary; line-height: 1.2;
|
|
|
+ &.highlight { color: $primary-color-dark; }
|
|
|
+ }
|
|
|
+ .stat-label { display: block; font-size: 20rpx; color: $text-color-muted; margin-top: 4rpx; line-height: 1.3;
|
|
|
+ &.highlight { color: $primary-color; font-weight: 600; }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// ====== Quick actions ======
|
|
|
-.quick-row { display: flex; gap: 16rpx; margin: 0 24rpx 16rpx; }
|
|
|
-.quick-btn { display: flex; align-items: center; gap: 12rpx; flex: 1; padding: 20rpx 24rpx; border-radius: $radius-base; background: $bg-color-card; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
- .quick-label { font-size: $font-size-base; font-weight: 500; color: $text-color-primary; }
|
|
|
- &:active { opacity: 0.7; }
|
|
|
+// ====== 搜索栏 ======
|
|
|
+.search-bar {
|
|
|
+ display: flex; align-items: center; gap: 12rpx; padding: 0 24rpx; margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.search-input-wrap {
|
|
|
+ flex: 1; display: flex; align-items: center; background: $bg-color-card;
|
|
|
+ border-radius: $radius-base; padding: 0 16rpx; height: 72rpx;
|
|
|
+ border: 1rpx solid $border-color;
|
|
|
+ .search-icon { font-size: 28rpx; margin-right: 8rpx; }
|
|
|
+ .search-input { flex: 1; font-size: $font-size-base; color: $text-color-primary; }
|
|
|
}
|
|
|
-.quick-icon { width: 40rpx; height: 40rpx; border-radius: $radius-sm; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
|
-.doc-icon { position: relative;
|
|
|
- &::before { content: ''; position: absolute; inset: 4rpx; border: 2.5rpx solid $primary-color; border-radius: 4rpx; }
|
|
|
- &::after { content: ''; position: absolute; top: 16rpx; left: 12rpx; right: 12rpx; height: 2.5rpx; background: $primary-color-dark; border-radius: 1rpx; box-shadow: 0 8rpx 0 $primary-color-dark; }
|
|
|
+.search-placeholder { color: $text-color-placeholder; }
|
|
|
+.search-btn {
|
|
|
+ padding: 0 28rpx; height: 72rpx; line-height: 72rpx;
|
|
|
+ background: $primary-color; border-radius: $radius-base;
|
|
|
+ font-size: $font-size-base; font-weight: 600; color: #1e293b;
|
|
|
+ &:active { opacity: 0.8; }
|
|
|
}
|
|
|
-.scan-icon-css { background: $primary-color; position: relative;
|
|
|
- &::before, &::after { content: ''; position: absolute; width: 12rpx; height: 12rpx; border: 3rpx solid #fff; border-radius: 3rpx; }
|
|
|
- &::before { top: 8rpx; left: 8rpx; }
|
|
|
- &::after { bottom: 8rpx; right: 8rpx; }
|
|
|
+.filter-btn {
|
|
|
+ display: flex; align-items: center; gap: 4rpx; padding: 0 20rpx; height: 72rpx; line-height: 72rpx;
|
|
|
+ background: $bg-color-card; border-radius: $radius-base; border: 1rpx solid $border-color;
|
|
|
+ font-size: $font-size-sm; color: $text-color-secondary;
|
|
|
+ .filter-icon { font-size: 18rpx; }
|
|
|
+ &:active { opacity: 0.7; }
|
|
|
}
|
|
|
|
|
|
-// ====== Loading ======
|
|
|
+// ====== 加载 ======
|
|
|
.load-tip { display: flex; justify-content: center; padding: 100rpx 0; }
|
|
|
.dot-row { display: flex; gap: $spacing-1; }
|
|
|
.pulse-dot { width: 10rpx; height: 10rpx; background: $text-color-placeholder; border-radius: 50%; animation: pulse 1.2s ease-in-out infinite;
|
|
|
@@ -260,48 +456,97 @@ onShow(async () => {
|
|
|
}
|
|
|
@keyframes pulse { 0%,80%,100% { transform: scale(0.5); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
|
|
|
|
|
|
-// ====== Device ======
|
|
|
+// ====== 设备列表 ======
|
|
|
.device-section { padding: 0 24rpx; }
|
|
|
-.section-header { display: flex; align-items: center; justify-content: space-between; padding: $spacing-2 0;
|
|
|
- .section-title { font-size: $font-size-md; font-weight: 600; color: $text-color-primary; }
|
|
|
- .section-count { font-size: $font-size-sm; color: $text-color-muted; }
|
|
|
-}
|
|
|
|
|
|
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0;
|
|
|
- .empty-icon { width: 120rpx; height: 120rpx; background: $bg-color-secondary; border-radius: $radius-xl; display: flex; align-items: center; justify-content: center; margin-bottom: $spacing-3; }
|
|
|
- .icon-device-empty { width: 44rpx; height: 36rpx; border: 3rpx solid $text-color-placeholder; border-radius: 6rpx; position: relative;
|
|
|
- &::after { content: ''; position: absolute; bottom: -8rpx; left: 50%; transform: translateX(-50%); width: 22rpx; height: 6rpx; background: $text-color-placeholder; border-radius: 0 0 4rpx 4rpx; }
|
|
|
- }
|
|
|
.empty-text { font-size: $font-size-base; color: $text-color-secondary; margin-bottom: $spacing-1; }
|
|
|
.empty-hint { font-size: $font-size-sm; color: $text-color-muted; }
|
|
|
}
|
|
|
|
|
|
-.device-card { margin-bottom: 16rpx; padding: $spacing-3; background: $bg-color-card; border-radius: $radius-md; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
+.device-card {
|
|
|
+ position: relative;
|
|
|
+ margin-bottom: 16rpx; padding: 24rpx; padding-right: 72rpx;
|
|
|
+ background: $bg-color-card; border-radius: $radius-md;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
&:active { opacity: 0.7; }
|
|
|
}
|
|
|
-.card-head { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 12rpx;
|
|
|
- .device-info { flex: 1;
|
|
|
- .device-name { display: block; font-size: $font-size-base; font-weight: 600; color: $text-color-primary; margin-bottom: 4rpx; }
|
|
|
- .device-sn { font-size: 22rpx; color: $text-color-muted; }
|
|
|
+.card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx;
|
|
|
+ .device-name { font-size: $font-size-base; font-weight: 600; color: $text-color-primary; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+ .order-tag { padding: 4rpx 16rpx; background: $primary-color; border-radius: 6rpx; font-size: 22rpx; font-weight: 600; color: #1e293b; flex-shrink: 0; margin-left: 12rpx; }
|
|
|
+}
|
|
|
+.card-body { display: flex; align-items: flex-start; gap: 16rpx;
|
|
|
+ .inventory-badge {
|
|
|
+ width: 100rpx; padding: 12rpx 0; border-radius: $radius-sm; text-align: center; flex-shrink: 0;
|
|
|
+ .badge-label { display: block; font-size: 20rpx; color: #fff; }
|
|
|
+ .badge-value { display: block; font-size: 32rpx; font-weight: 800; color: #fff; }
|
|
|
+ &.danger { background: $error-color; }
|
|
|
+ &.warning { background: $accent-color; }
|
|
|
+ &.normal { background: $success-color; }
|
|
|
}
|
|
|
- .status-tag { display: flex; align-items: center; gap: 6rpx; padding: 6rpx 14rpx; border-radius: $radius-full; font-size: 22rpx; font-weight: 500; flex-shrink: 0;
|
|
|
- .status-dot { width: 10rpx; height: 10rpx; border-radius: 50%; }
|
|
|
- &.online { background: $success-color-bg; color: $success-color; .status-dot { background: $success-color; } }
|
|
|
- &.offline { background: $bg-color-secondary; color: $text-color-muted; .status-dot { background: $text-color-placeholder; } }
|
|
|
+ .metrics-row { flex: 1; display: flex; flex-wrap: wrap;
|
|
|
+ .metric-item { width: 50%; margin-bottom: 8rpx;
|
|
|
+ .metric-value { font-size: $font-size-base; font-weight: 700; color: $text-color-primary; }
|
|
|
+ .metric-label { font-size: 20rpx; color: $text-color-muted; margin-left: 4rpx; }
|
|
|
+ .info-tip { font-size: 14rpx; color: $warning-color; font-style: italic; margin-left: 2rpx; }
|
|
|
+ }
|
|
|
}
|
|
|
+ .card-arrow { position: absolute; right: 72rpx; top: 50%; transform: translateY(-50%); font-size: 28rpx; color: $text-color-placeholder; }
|
|
|
}
|
|
|
-.device-addr { margin-bottom: 12rpx; text { font-size: $font-size-sm; color: $text-color-muted; } }
|
|
|
-.stock-row { display: flex; gap: 12rpx; margin-bottom: 16rpx;
|
|
|
- .stock-chip { flex: 1; text-align: center; padding: 12rpx 8rpx; background: $bg-color-page; border-radius: $radius-sm;
|
|
|
- .chip-label { display: block; font-size: 20rpx; color: $text-color-muted; margin-bottom: 4rpx; }
|
|
|
- .chip-value { font-size: $font-size-base; font-weight: 700; color: $text-color-primary; }
|
|
|
- &.warn { background: $warning-color-bg; .chip-value { color: $warning-color; } }
|
|
|
- &.danger { background: $error-color-bg; .chip-value { color: $error-color; } }
|
|
|
- .chip-value.accent { color: $accent-color; }
|
|
|
+.card-checkbox {
|
|
|
+ position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
|
|
|
+ width: 40rpx; height: 40rpx; border-radius: 8rpx; border: 3rpx solid $border-color;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ &.checked { background: $primary-color; border-color: $primary-color;
|
|
|
+ .check-mark { color: #fff; font-size: 24rpx; font-weight: 700; }
|
|
|
}
|
|
|
}
|
|
|
-.card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 12rpx; border-top: 1rpx solid $border-color-light;
|
|
|
- .foot-text { font-size: $font-size-sm; color: $primary-color; font-weight: 500; }
|
|
|
- .foot-arrow { font-size: $font-size-sm; color: $text-color-placeholder; }
|
|
|
+
|
|
|
+// ====== 底部批量操作栏 ======
|
|
|
+.bottom-bar {
|
|
|
+ position: fixed; bottom: 0; left: 0; right: 0;
|
|
|
+ display: flex; align-items: center; justify-content: space-between;
|
|
|
+ padding: 16rpx 24rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
|
|
+ background: $bg-color-card; border-top: 1rpx solid $border-color-light;
|
|
|
+ z-index: 100;
|
|
|
+}
|
|
|
+.bottom-left { display: flex; align-items: center; gap: 16rpx;
|
|
|
+ .select-all-btn { display: flex; align-items: center; gap: 8rpx; font-size: $font-size-sm; color: $text-color-secondary;
|
|
|
+ .checkbox-icon { width: 36rpx; height: 36rpx; border-radius: 8rpx; border: 3rpx solid $border-color;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ &.checked { background: $primary-color; border-color: $primary-color;
|
|
|
+ .check-mark { color: #fff; font-size: 22rpx; font-weight: 700; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .selected-count { font-size: $font-size-sm; color: $text-color-muted; }
|
|
|
+}
|
|
|
+.batch-btn {
|
|
|
+ padding: 16rpx 32rpx; background: $primary-color; border-radius: $radius-base;
|
|
|
+ font-size: $font-size-base; font-weight: 600; color: #1e293b;
|
|
|
+ &:active { opacity: 0.8; }
|
|
|
+}
|
|
|
+
|
|
|
+// ====== 阈值弹窗 ======
|
|
|
+.modal-mask {
|
|
|
+ position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9999;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+}
|
|
|
+.modal-content {
|
|
|
+ width: 560rpx; background: $bg-color-card; border-radius: $radius-lg; padding: 32rpx;
|
|
|
+}
|
|
|
+.modal-title { display: block; font-size: $font-size-lg; font-weight: 700; color: $text-color-primary; text-align: center; margin-bottom: 24rpx; }
|
|
|
+.modal-input-wrap {
|
|
|
+ display: flex; align-items: center; background: $bg-color-page; border-radius: $radius-base;
|
|
|
+ padding: 0 20rpx; height: 80rpx; border: 1rpx solid $border-color;
|
|
|
+ .modal-input { flex: 1; font-size: $font-size-lg; color: $text-color-primary; text-align: center; }
|
|
|
+ .modal-suffix { font-size: $font-size-lg; color: $text-color-muted; margin-left: 8rpx; }
|
|
|
+}
|
|
|
+.modal-actions { display: flex; gap: 16rpx; margin-top: 24rpx;
|
|
|
+ .modal-btn { flex: 1; height: 72rpx; line-height: 72rpx; text-align: center; border-radius: $radius-base; font-size: $font-size-base; font-weight: 600;
|
|
|
+ &.cancel { background: $bg-color-page; color: $text-color-secondary; }
|
|
|
+ &.confirm { background: $primary-color; color: #1e293b; }
|
|
|
+ &:active { opacity: 0.8; }
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|