|
|
@@ -1,416 +1,824 @@
|
|
|
<template>
|
|
|
<view class="page">
|
|
|
- <!-- 导航栏 -->
|
|
|
- <NavBar title="商品管理" :showBack="true" />
|
|
|
-
|
|
|
- <!-- 分类筛选 -->
|
|
|
- <view class="filter-section">
|
|
|
- <scroll-view class="category-scroll" scroll-x>
|
|
|
- <view class="category-list">
|
|
|
- <view
|
|
|
- class="category-item"
|
|
|
- :class="{ active: currentCategory === '' }"
|
|
|
- @click="changeCategory('')"
|
|
|
- >
|
|
|
- <text>全部</text>
|
|
|
- </view>
|
|
|
- <view
|
|
|
- class="category-item"
|
|
|
- v-for="cat in categories"
|
|
|
- :key="cat.id"
|
|
|
- :class="{ active: currentCategory === cat.name }"
|
|
|
- @click="changeCategory(cat.name)"
|
|
|
- >
|
|
|
- <text>{{ cat.name }}</text>
|
|
|
- </view>
|
|
|
+ <!-- 黄色头部区域 -->
|
|
|
+ <view class="header-section">
|
|
|
+ <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
|
|
+ <view class="header-nav">
|
|
|
+ <view class="back-btn" @click="goBack">
|
|
|
+ <text class="back-icon">←</text>
|
|
|
</view>
|
|
|
- </scroll-view>
|
|
|
+ <text class="header-title">{{ deviceId }}配置上架商品</text>
|
|
|
+ <view class="header-right"></view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- 商品列表 -->
|
|
|
- <scroll-view
|
|
|
- class="product-scroll"
|
|
|
- scroll-y
|
|
|
- @scrolltolower="loadMore"
|
|
|
- >
|
|
|
- <view class="product-list">
|
|
|
- <view
|
|
|
- class="product-card"
|
|
|
- v-for="product in productList"
|
|
|
- :key="product.id"
|
|
|
+
|
|
|
+ <!-- 楼层Tab切换 -->
|
|
|
+ <view class="floor-tabs">
|
|
|
+ <view class="floor-grid">
|
|
|
+ <view
|
|
|
+ class="floor-item"
|
|
|
+ v-for="floor in floors"
|
|
|
+ :key="floor.floorNumber"
|
|
|
+ :class="{ active: currentFloor === floor.floorNumber }"
|
|
|
+ @click="scrollToFloor(floor.floorNumber)"
|
|
|
>
|
|
|
- <view class="product-image">
|
|
|
- <image v-if="product.image" :src="product.image" mode="aspectFill" />
|
|
|
- <view v-else class="image-placeholder">
|
|
|
- <view class="box-icon"></view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="product-content">
|
|
|
- <view class="product-info">
|
|
|
- <text class="product-name">{{ product.name }}</text>
|
|
|
- <text class="product-category">{{ product.category }}</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="product-footer">
|
|
|
- <text class="product-price">¥{{ formatMoney(product.price) }}</text>
|
|
|
- <view class="status-tag" :class="getStatusClass(product.status)">
|
|
|
- <text>{{ getStatusText(product.status) }}</text>
|
|
|
+ <text class="floor-text">{{ floor.floorName }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 内容滚动区 -->
|
|
|
+ <scroll-view
|
|
|
+ class="content-scroll"
|
|
|
+ scroll-y
|
|
|
+ :scroll-into-view="scrollIntoViewId"
|
|
|
+ @scroll="onContentScroll"
|
|
|
+ >
|
|
|
+ <!-- 复制模板浮动按钮 -->
|
|
|
+ <view class="template-float" @click="copyTemplate">
|
|
|
+ <text class="template-float-text">复制模板 ❯❯</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 所有楼层 -->
|
|
|
+ <view
|
|
|
+ class="floor-section"
|
|
|
+ v-for="floor in floors"
|
|
|
+ :key="floor.floorNumber"
|
|
|
+ :id="'floor-' + floor.floorNumber"
|
|
|
+ >
|
|
|
+ <!-- 楼层提示 -->
|
|
|
+ <view class="floor-hint">
|
|
|
+ <text class="hint-text">{{ floor.floorName }}(点击商品图片可修改价格)</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 货道商品网格 -->
|
|
|
+ <view class="slot-grid">
|
|
|
+ <view
|
|
|
+ class="slot-card"
|
|
|
+ v-for="slot in floor.slots"
|
|
|
+ :key="slot.slotId"
|
|
|
+ :class="{ empty: !slot.productId }"
|
|
|
+ >
|
|
|
+ <!-- 有商品的货道 -->
|
|
|
+ <template v-if="slot.productId">
|
|
|
+ <view class="slot-image-wrap" @click="editPrice(slot)">
|
|
|
+ <image
|
|
|
+ v-if="slot.productImage"
|
|
|
+ class="slot-image"
|
|
|
+ :src="slot.productImage"
|
|
|
+ mode="aspectFill"
|
|
|
+ />
|
|
|
+ <view v-else class="slot-image-placeholder">
|
|
|
+ <text class="placeholder-text">暂无图片</text>
|
|
|
+ </view>
|
|
|
+ <!-- 删除按钮 -->
|
|
|
+ <view class="slot-delete" @click.stop="removeProduct(slot)">
|
|
|
+ <text class="delete-icon">×</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="product-stats">
|
|
|
- <view class="stat">
|
|
|
- <text class="stat-label">库存</text>
|
|
|
- <text class="stat-value">{{ product.totalStock }}</text>
|
|
|
+ <!-- 位置编号+价格条带 -->
|
|
|
+ <view class="slot-meta-bar">
|
|
|
+ <text class="meta-position">{{ slot.slotId }}</text>
|
|
|
+ <text class="meta-price">¥{{ formatPrice(slot.price) }}</text>
|
|
|
</view>
|
|
|
- <view class="stat">
|
|
|
- <text class="stat-label">今日</text>
|
|
|
- <text class="stat-value">{{ product.todaySales }}</text>
|
|
|
+ <view class="slot-info">
|
|
|
+ <text class="slot-name">{{ slot.productName }}</text>
|
|
|
+ <text class="slot-stock-label">预设库存数量</text>
|
|
|
+ <view class="slot-stock-control">
|
|
|
+ <view class="stock-btn minus" @click="changeStock(slot, -1)">
|
|
|
+ <text class="stock-btn-text">−</text>
|
|
|
+ </view>
|
|
|
+ <input
|
|
|
+ class="stock-input"
|
|
|
+ type="number"
|
|
|
+ v-model.number="slot.stock"
|
|
|
+ @blur="validateStock(slot)"
|
|
|
+ />
|
|
|
+ <view class="stock-btn plus" @click="changeStock(slot, 1)">
|
|
|
+ <text class="stock-btn-text">+</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="stat">
|
|
|
- <text class="stat-label">月销</text>
|
|
|
- <text class="stat-value">{{ product.monthSales }}</text>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 空货道 -->
|
|
|
+ <template v-else>
|
|
|
+ <view class="slot-add" @click="addProduct(slot)">
|
|
|
+ <text class="add-icon">+</text>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </template>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
-
|
|
|
- <view class="loading-more" v-if="loading">
|
|
|
- <view class="loading-spinner"></view>
|
|
|
- <text>加载中...</text>
|
|
|
- </view>
|
|
|
-
|
|
|
- <view class="no-more" v-if="!hasMore && productList.length > 0">
|
|
|
- <text>— 没有更多了 —</text>
|
|
|
+
|
|
|
+ <!-- 底部留白 -->
|
|
|
+ <view class="bottom-spacer"></view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <!-- 底部保存按钮 -->
|
|
|
+ <view class="footer-section">
|
|
|
+ <view class="save-btn" @click="saveConfig">
|
|
|
+ <text class="save-btn-text">保存</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <view class="empty-state" v-if="!loading && productList.length === 0">
|
|
|
- <view class="empty-icon">
|
|
|
- <view class="empty-icon-inner"></view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 价格编辑弹窗 -->
|
|
|
+ <view class="price-modal" v-if="showPriceModal" @click="closePriceModal">
|
|
|
+ <view class="price-modal-content" @click.stop>
|
|
|
+ <view class="price-modal-header">
|
|
|
+ <text class="price-modal-title">修改价格</text>
|
|
|
+ <text class="price-modal-close" @click="closePriceModal">×</text>
|
|
|
+ </view>
|
|
|
+ <view class="price-modal-body">
|
|
|
+ <text class="price-modal-label">商品价格</text>
|
|
|
+ <view class="price-input-wrap">
|
|
|
+ <text class="price-symbol">¥</text>
|
|
|
+ <input
|
|
|
+ class="price-input"
|
|
|
+ type="digit"
|
|
|
+ v-model="editingPrice"
|
|
|
+ placeholder="请输入价格"
|
|
|
+ focus
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="price-modal-footer">
|
|
|
+ <view class="price-modal-btn cancel" @click="closePriceModal">
|
|
|
+ <text>取消</text>
|
|
|
+ </view>
|
|
|
+ <view class="price-modal-btn confirm" @click="confirmPrice">
|
|
|
+ <text>确定</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <text class="empty-text">暂无商品数据</text>
|
|
|
</view>
|
|
|
- </scroll-view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted } from 'vue';
|
|
|
-import NavBar from '@/components/NavBar.vue';
|
|
|
-import { getProductList } from '@/api/product';
|
|
|
-import { ProductStatusText, ProductStatusColor } from '@/utils/constants';
|
|
|
-import { formatMoney as formatMoneyUtil } from '@/utils/common';
|
|
|
-
|
|
|
-const productList = ref<any[]>([]);
|
|
|
-const categories = ref<any[]>([]);
|
|
|
-const currentCategory = ref('');
|
|
|
-const loading = ref(false);
|
|
|
-const hasMore = ref(true);
|
|
|
-const page = ref(1);
|
|
|
-const pageSize = 10;
|
|
|
-
|
|
|
-const formatMoney = formatMoneyUtil;
|
|
|
-
|
|
|
-const getStatusText = (status: number) => ProductStatusText[status] || '未知';
|
|
|
-
|
|
|
-const getStatusClass = (status: number) => {
|
|
|
- if (status === 1) return 'active';
|
|
|
- return 'inactive';
|
|
|
+import { ref, computed, onMounted } from 'vue';
|
|
|
+import { formatMoney } from '@/utils/common';
|
|
|
+import { getDeviceProductConfig, saveDeviceProductConfig } from '@/api/device';
|
|
|
+
|
|
|
+const statusBarHeight = ref(44);
|
|
|
+
|
|
|
+// 获取设备ID
|
|
|
+const deviceId = ref('');
|
|
|
+
|
|
|
+// 楼层数据
|
|
|
+interface SlotItem {
|
|
|
+ slotId: string;
|
|
|
+ productId?: string;
|
|
|
+ productName?: string;
|
|
|
+ productImage?: string;
|
|
|
+ price?: number;
|
|
|
+ stock?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface FloorItem {
|
|
|
+ floorNumber: number;
|
|
|
+ floorLabel: string;
|
|
|
+ floorName: string;
|
|
|
+ slots: SlotItem[];
|
|
|
+}
|
|
|
+
|
|
|
+const floors = ref<FloorItem[]>([]);
|
|
|
+const currentFloor = ref(1);
|
|
|
+const scrollIntoViewId = ref('');
|
|
|
+
|
|
|
+// 价格编辑弹窗
|
|
|
+const showPriceModal = ref(false);
|
|
|
+const editingPrice = ref('');
|
|
|
+const editingSlot = ref<SlotItem | null>(null);
|
|
|
+
|
|
|
+// 初始化状态栏高度
|
|
|
+const initStatusBar = () => {
|
|
|
+ try {
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
+ statusBarHeight.value = systemInfo.statusBarHeight || 44;
|
|
|
+ } catch (e) {
|
|
|
+ statusBarHeight.value = 44;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 生成Mock数据
|
|
|
+const generateMockData = () => {
|
|
|
+ const floorConfigs = [
|
|
|
+ { floorNumber: 1, floorLabel: '第1层\n(顶层)', floorName: '第1层', slotCount: 3 },
|
|
|
+ { floorNumber: 2, floorLabel: '第2层', floorName: '第2层', slotCount: 5 },
|
|
|
+ { floorNumber: 3, floorLabel: '第3层', floorName: '第3层', slotCount: 5 },
|
|
|
+ { floorNumber: 4, floorLabel: '第4层', floorName: '第4层', slotCount: 5 },
|
|
|
+ { floorNumber: 5, floorLabel: '第5层\n(底层)', floorName: '第5层', slotCount: 3 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const mockProducts = [
|
|
|
+ { id: 'P001', name: '白象红烧牛肉面面饼120g', price: 500, image: '' },
|
|
|
+ { id: 'P002', name: '白象老坛酸菜牛肉面面饼1...', price: 500, image: '' },
|
|
|
+ { id: 'P003', name: '招牌卤肉饭(盖浇)', price: 1500, image: '' },
|
|
|
+ { id: 'P004', name: '土豆牛腩饭(盖浇)', price: 1800, image: '' },
|
|
|
+ { id: 'P005', name: '辣椒炒肉饭(盖浇)', price: 1500, image: '' },
|
|
|
+ { id: 'P006', name: '农家一碗香饭(盖浇)', price: 1500, image: '' },
|
|
|
+ { id: 'P007', name: '白米饭', price: 100, image: '' },
|
|
|
+ { id: 'P008', name: '可口可乐330ml', price: 350, image: '' },
|
|
|
+ { id: 'P009', name: '矿泉水550ml', price: 200, image: '' },
|
|
|
+ { id: 'P010', name: '薯片40g', price: 680, image: '' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ let productIndex = 0;
|
|
|
+
|
|
|
+ floors.value = floorConfigs.map(config => {
|
|
|
+ const slots: SlotItem[] = [];
|
|
|
+ for (let i = 1; i <= config.slotCount; i++) {
|
|
|
+ const slotId = `${config.floorNumber}-${i}`;
|
|
|
+ // 部分货道有商品,部分为空
|
|
|
+ if (productIndex < mockProducts.length && Math.random() > 0.2) {
|
|
|
+ const product = mockProducts[productIndex++];
|
|
|
+ slots.push({
|
|
|
+ slotId,
|
|
|
+ productId: product.id,
|
|
|
+ productName: product.name,
|
|
|
+ productImage: product.image,
|
|
|
+ price: product.price,
|
|
|
+ stock: Math.floor(Math.random() * 10)
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ slots.push({ slotId });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ floorNumber: config.floorNumber,
|
|
|
+ floorLabel: config.floorLabel,
|
|
|
+ floorName: config.floorName,
|
|
|
+ slots
|
|
|
+ };
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化价格(分转元)
|
|
|
+const formatPrice = (price?: number) => {
|
|
|
+ if (price === undefined || price === null) return '0.00';
|
|
|
+ return formatMoney(price);
|
|
|
+};
|
|
|
+
|
|
|
+// 返回上一页
|
|
|
+const goBack = () => {
|
|
|
+ uni.navigateBack({ delta: 1 });
|
|
|
+};
|
|
|
+
|
|
|
+// 切换楼层(滚动定位)
|
|
|
+const scrollToFloor = (floorNumber: number) => {
|
|
|
+ currentFloor.value = floorNumber;
|
|
|
+ scrollIntoViewId.value = 'floor-' + floorNumber;
|
|
|
+ // 清除scrollIntoViewId,以便下次能再次触发
|
|
|
+ setTimeout(() => {
|
|
|
+ scrollIntoViewId.value = '';
|
|
|
+ }, 300);
|
|
|
+};
|
|
|
+
|
|
|
+// 监听滚动,更新当前楼层
|
|
|
+const onContentScroll = (e: any) => {
|
|
|
+ // 简化处理:根据粗略的楼层高度估算当前楼层
|
|
|
+ const scrollTop = e.detail.scrollTop;
|
|
|
+ const approximateFloorHeight = 400;
|
|
|
+ const floorIndex = Math.floor(scrollTop / approximateFloorHeight);
|
|
|
+ if (floorIndex >= 0 && floorIndex < floors.value.length) {
|
|
|
+ currentFloor.value = floors.value[floorIndex].floorNumber;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-const changeCategory = (category: string) => {
|
|
|
- currentCategory.value = category;
|
|
|
- page.value = 1;
|
|
|
- hasMore.value = true;
|
|
|
- loadProducts();
|
|
|
+// 修改库存
|
|
|
+const changeStock = (slot: SlotItem, delta: number) => {
|
|
|
+ if (slot.stock === undefined) slot.stock = 0;
|
|
|
+ slot.stock = Math.max(0, slot.stock + delta);
|
|
|
};
|
|
|
|
|
|
-const loadProducts = async () => {
|
|
|
- loading.value = true;
|
|
|
+// 校验库存
|
|
|
+const validateStock = (slot: SlotItem) => {
|
|
|
+ if (slot.stock === undefined || slot.stock === null || isNaN(slot.stock)) {
|
|
|
+ slot.stock = 0;
|
|
|
+ }
|
|
|
+ slot.stock = Math.max(0, Math.floor(slot.stock));
|
|
|
+};
|
|
|
+
|
|
|
+// 删除商品
|
|
|
+const removeProduct = (slot: SlotItem) => {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '确定要移除该商品吗?',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ slot.productId = undefined;
|
|
|
+ slot.productName = undefined;
|
|
|
+ slot.productImage = undefined;
|
|
|
+ slot.price = undefined;
|
|
|
+ slot.stock = undefined;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 添加商品
|
|
|
+const addProduct = (slot: SlotItem) => {
|
|
|
+ uni.showToast({ title: '选择商品功能开发中', icon: 'none' });
|
|
|
+ // TODO: 跳转到商品选择页面
|
|
|
+ // uni.navigateTo({ url: `/pages/products/select?deviceId=${deviceId.value}&slotId=${slot.slotId}` });
|
|
|
+};
|
|
|
+
|
|
|
+// 编辑价格(后端单位:元)
|
|
|
+const editPrice = (slot: SlotItem) => {
|
|
|
+ editingSlot.value = slot;
|
|
|
+ editingPrice.value = slot.price ? String(slot.price) : '';
|
|
|
+ showPriceModal.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭价格弹窗
|
|
|
+const closePriceModal = () => {
|
|
|
+ showPriceModal.value = false;
|
|
|
+ editingSlot.value = null;
|
|
|
+ editingPrice.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+// 确认价格(单位:元)
|
|
|
+const confirmPrice = () => {
|
|
|
+ const price = parseFloat(editingPrice.value);
|
|
|
+ if (isNaN(price) || price < 0) {
|
|
|
+ uni.showToast({ title: '请输入有效的价格', icon: 'none' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (editingSlot.value) {
|
|
|
+ editingSlot.value.price = price;
|
|
|
+ }
|
|
|
+ closePriceModal();
|
|
|
+};
|
|
|
+
|
|
|
+// 复制模板
|
|
|
+const copyTemplate = () => {
|
|
|
+ uni.showToast({ title: '复制模板功能开发中', icon: 'none' });
|
|
|
+};
|
|
|
+
|
|
|
+// 加载商品配置
|
|
|
+const loadProductConfig = async () => {
|
|
|
+ if (!deviceId.value) return;
|
|
|
try {
|
|
|
- const res = await getProductList({
|
|
|
- page: page.value,
|
|
|
- pageSize,
|
|
|
- category: currentCategory.value
|
|
|
- });
|
|
|
-
|
|
|
- if (page.value === 1) {
|
|
|
- productList.value = res.list;
|
|
|
+ uni.showLoading({ title: '加载中...', mask: true });
|
|
|
+ const res = await getDeviceProductConfig(deviceId.value);
|
|
|
+ uni.hideLoading();
|
|
|
+ if (res && res.floors && res.floors.length > 0) {
|
|
|
+ floors.value = res.floors;
|
|
|
} else {
|
|
|
- productList.value = [...productList.value, ...res.list];
|
|
|
+ generateMockData();
|
|
|
}
|
|
|
-
|
|
|
- hasMore.value = productList.value.length < res.total;
|
|
|
} catch (error) {
|
|
|
- console.error('加载商品列表失败', error);
|
|
|
- } finally {
|
|
|
- loading.value = false;
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({ title: '加载失败', icon: 'none' });
|
|
|
+ generateMockData();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const loadMore = () => {
|
|
|
- if (loading.value || !hasMore.value) return;
|
|
|
- page.value++;
|
|
|
- loadProducts();
|
|
|
+// 保存配置
|
|
|
+const saveConfig = async () => {
|
|
|
+ if (!deviceId.value) return;
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '保存中...', mask: true });
|
|
|
+ await saveDeviceProductConfig(deviceId.value, { floors: floors.value });
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({ title: '保存成功', icon: 'success' });
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({ title: '保存失败', icon: 'none' });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
+// 页面加载
|
|
|
onMounted(() => {
|
|
|
- loadProducts();
|
|
|
+ initStatusBar();
|
|
|
+ // 获取页面参数
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ const currentPage = pages[pages.length - 1] as any;
|
|
|
+ const options = currentPage?.options || {};
|
|
|
+ deviceId.value = options.deviceId || 'B150360';
|
|
|
+ // 加载真实接口数据
|
|
|
+ loadProductConfig();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.page {
|
|
|
- min-height: 100vh;
|
|
|
- background: #f8fafc;
|
|
|
+ height: 100vh;
|
|
|
+ background: #f5f5f5;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-/* 分类筛选 */
|
|
|
-.filter-section {
|
|
|
- padding: 12rpx 0;
|
|
|
- background: #ffffff;
|
|
|
- border-bottom: 1rpx solid #e2e8f0;
|
|
|
-}
|
|
|
-
|
|
|
-.category-scroll {
|
|
|
- white-space: nowrap;
|
|
|
-}
|
|
|
-
|
|
|
-.category-list {
|
|
|
- display: inline-flex;
|
|
|
- padding: 0 24rpx;
|
|
|
- gap: 10rpx;
|
|
|
-
|
|
|
- .category-item {
|
|
|
- padding: 12rpx 24rpx;
|
|
|
- background: #f8fafc;
|
|
|
- border: 2rpx solid #e2e8f0;
|
|
|
- border-radius: 20rpx;
|
|
|
- font-size: 26rpx;
|
|
|
- color: #64748b;
|
|
|
- transition: all 0.15s;
|
|
|
-
|
|
|
- &.active {
|
|
|
- background: #ecfdf5;
|
|
|
- border-color: #10b981;
|
|
|
- color: #10b981;
|
|
|
- }
|
|
|
+/* 黄色头部区域 */
|
|
|
+.header-section {
|
|
|
+ background: #FFD93D;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.status-bar {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.header-nav {
|
|
|
+ height: 88rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 0 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.back-btn {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.back-icon {
|
|
|
+ font-size: 40rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ text-align: center;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.header-right {
|
|
|
+ width: 60rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 楼层Tab */
|
|
|
+.floor-tabs {
|
|
|
+ background: #FFD93D;
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.floor-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(5, 1fr);
|
|
|
+ gap: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.floor-item {
|
|
|
+ padding: 20rpx 12rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ text-align: center;
|
|
|
+ border: 3rpx solid transparent;
|
|
|
+ transition: all 0.2s;
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background: #fff;
|
|
|
+ border-color: #333;
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* 商品列表 */
|
|
|
-.product-scroll {
|
|
|
+.floor-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+/* 内容滚动区 */
|
|
|
+.content-scroll {
|
|
|
flex: 1;
|
|
|
height: 0;
|
|
|
+ padding: 20rpx;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.product-list {
|
|
|
- padding: 16rpx 24rpx;
|
|
|
+/* 复制模板浮动按钮 */
|
|
|
+.template-float {
|
|
|
+ position: absolute;
|
|
|
+ top: 20rpx;
|
|
|
+ right: 20rpx;
|
|
|
+ z-index: 10;
|
|
|
+ background: rgba(255, 217, 61, 0.9);
|
|
|
+ padding: 10rpx 20rpx;
|
|
|
+ border-radius: 24rpx;
|
|
|
}
|
|
|
|
|
|
-.product-card {
|
|
|
+.template-float-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+/* 楼层区块 */
|
|
|
+.floor-section {
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 楼层提示 */
|
|
|
+.floor-hint {
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.hint-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 货道网格 */
|
|
|
+.slot-grid {
|
|
|
display: flex;
|
|
|
- background: #ffffff;
|
|
|
- border: 1rpx solid #e2e8f0;
|
|
|
- border-radius: 16rpx;
|
|
|
- margin-bottom: 12rpx;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-card {
|
|
|
+ width: calc((100% - 32rpx) / 3);
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
overflow: hidden;
|
|
|
- transition: transform 0.15s;
|
|
|
-
|
|
|
- &:active {
|
|
|
- transform: scale(0.98);
|
|
|
- }
|
|
|
-
|
|
|
- .product-image {
|
|
|
- width: 160rpx;
|
|
|
- height: 160rpx;
|
|
|
- flex-shrink: 0;
|
|
|
-
|
|
|
- image {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- object-fit: cover;
|
|
|
- }
|
|
|
-
|
|
|
- .image-placeholder {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- background: #f8fafc;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
-
|
|
|
- .box-icon {
|
|
|
- width: 32rpx;
|
|
|
- height: 32rpx;
|
|
|
- background: #e2e8f0;
|
|
|
- border-radius: 6rpx;
|
|
|
- position: relative;
|
|
|
-
|
|
|
- &::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- top: -8rpx;
|
|
|
- left: 50%;
|
|
|
- transform: translateX(-50%);
|
|
|
- width: 0;
|
|
|
- height: 0;
|
|
|
- border-left: 10rpx solid transparent;
|
|
|
- border-right: 10rpx solid transparent;
|
|
|
- border-bottom: 10rpx solid #e2e8f0;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .product-content {
|
|
|
- flex: 1;
|
|
|
- padding: 16rpx 20rpx;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: space-between;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
+
|
|
|
+ &.empty {
|
|
|
+ background: #fff;
|
|
|
+ min-height: 320rpx;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.product-info {
|
|
|
- .product-name {
|
|
|
- display: block;
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #1e293b;
|
|
|
- margin-bottom: 4rpx;
|
|
|
- }
|
|
|
-
|
|
|
- .product-category {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #94a3b8;
|
|
|
- }
|
|
|
+/* 有商品货道 */
|
|
|
+.slot-image-wrap {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 200rpx;
|
|
|
+ background: #f8f8f8;
|
|
|
}
|
|
|
|
|
|
-.product-footer {
|
|
|
+.slot-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-image-placeholder {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 10rpx;
|
|
|
- margin: 10rpx 0;
|
|
|
-
|
|
|
- .product-price {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: 700;
|
|
|
- color: #f97316;
|
|
|
- }
|
|
|
-
|
|
|
- .status-tag {
|
|
|
- padding: 4rpx 12rpx;
|
|
|
- border-radius: 6rpx;
|
|
|
- font-size: 20rpx;
|
|
|
-
|
|
|
- &.active {
|
|
|
- background: #ecfdf5;
|
|
|
- color: #10b981;
|
|
|
- }
|
|
|
-
|
|
|
- &.inactive {
|
|
|
- background: #fff7ed;
|
|
|
- color: #f97316;
|
|
|
- }
|
|
|
- }
|
|
|
+ justify-content: center;
|
|
|
+ background: #f0f0f0;
|
|
|
}
|
|
|
|
|
|
-.product-stats {
|
|
|
+.placeholder-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-delete {
|
|
|
+ position: absolute;
|
|
|
+ top: 6rpx;
|
|
|
+ right: 6rpx;
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 50%;
|
|
|
display: flex;
|
|
|
- gap: 20rpx;
|
|
|
-
|
|
|
- .stat {
|
|
|
- .stat-label {
|
|
|
- font-size: 20rpx;
|
|
|
- color: #94a3b8;
|
|
|
- margin-right: 4rpx;
|
|
|
- }
|
|
|
-
|
|
|
- .stat-value {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #475569;
|
|
|
- font-weight: 500;
|
|
|
- }
|
|
|
- }
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.delete-icon {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1;
|
|
|
}
|
|
|
|
|
|
-/* 加载状态 */
|
|
|
-.loading-more {
|
|
|
+/* 位置编号+价格条带 */
|
|
|
+.slot-meta-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ background: #ff4d4f;
|
|
|
+ padding: 6rpx 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-position {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-price {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-info {
|
|
|
+ padding: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-name {
|
|
|
+ display: block;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.4;
|
|
|
+ height: 62rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ margin-bottom: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-stock-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 18rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-bottom: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.slot-stock-control {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.stock-btn {
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- gap: 12rpx;
|
|
|
- padding: 32rpx;
|
|
|
- color: #94a3b8;
|
|
|
- font-size: 24rpx;
|
|
|
-
|
|
|
- .loading-spinner {
|
|
|
- width: 32rpx;
|
|
|
- height: 32rpx;
|
|
|
- border: 3rpx solid #e2e8f0;
|
|
|
- border-top-color: #10b981;
|
|
|
- border-radius: 50%;
|
|
|
- animation: spin 1s linear infinite;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
-@keyframes spin {
|
|
|
- to { transform: rotate(360deg); }
|
|
|
+.stock-btn-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1;
|
|
|
}
|
|
|
|
|
|
-.no-more {
|
|
|
+.stock-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 40rpx;
|
|
|
text-align: center;
|
|
|
- padding: 32rpx;
|
|
|
font-size: 24rpx;
|
|
|
- color: #cbd5e1;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
|
|
|
-.empty-state {
|
|
|
+/* 空货道 */
|
|
|
+.slot-add {
|
|
|
+ width: 100%;
|
|
|
+ height: 320rpx;
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
align-items: center;
|
|
|
- padding: 100rpx 0;
|
|
|
-
|
|
|
- .empty-icon {
|
|
|
- width: 120rpx;
|
|
|
- height: 120rpx;
|
|
|
- background: #f1f5f9;
|
|
|
- border-radius: 24rpx;
|
|
|
- margin-bottom: 20rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
-
|
|
|
- .empty-icon-inner {
|
|
|
- width: 40rpx;
|
|
|
- height: 28rpx;
|
|
|
- background: #cbd5e1;
|
|
|
- border-radius: 6rpx;
|
|
|
- position: relative;
|
|
|
-
|
|
|
- &::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- top: -10rpx;
|
|
|
- left: 50%;
|
|
|
- transform: translateX(-50%);
|
|
|
- width: 0;
|
|
|
- height: 0;
|
|
|
- border-left: 12rpx solid transparent;
|
|
|
- border-right: 12rpx solid transparent;
|
|
|
- border-bottom: 10rpx solid #cbd5e1;
|
|
|
- }
|
|
|
+ justify-content: center;
|
|
|
+ border: 2rpx dashed #e0e0e0;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.add-icon {
|
|
|
+ font-size: 60rpx;
|
|
|
+ color: #ddd;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部留白 */
|
|
|
+.bottom-spacer {
|
|
|
+ height: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部保存 */
|
|
|
+.footer-section {
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
|
+ background: #fff;
|
|
|
+ border-top: 1rpx solid #eee;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.save-btn {
|
|
|
+ height: 88rpx;
|
|
|
+ background: #FFD93D;
|
|
|
+ border-radius: 44rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.save-btn-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+/* 价格弹窗 */
|
|
|
+.price-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-content {
|
|
|
+ width: 560rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 30rpx;
|
|
|
+ border-bottom: 1rpx solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-close {
|
|
|
+ font-size: 40rpx;
|
|
|
+ color: #999;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-body {
|
|
|
+ padding: 40rpx 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.price-input-wrap {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-radius: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.price-symbol {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.price-input {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-footer {
|
|
|
+ display: flex;
|
|
|
+ border-top: 1rpx solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.price-modal-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 96rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ text {
|
|
|
+ font-size: 30rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.cancel {
|
|
|
+ border-right: 1rpx solid #eee;
|
|
|
+
|
|
|
+ text {
|
|
|
+ color: #666;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- .empty-text {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #94a3b8;
|
|
|
+
|
|
|
+ &.confirm {
|
|
|
+ text {
|
|
|
+ color: #FFD93D;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|