Files
medical-mall/pages/mall/admin/product-management.uvue

1755 lines
40 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<AdminLayout current-page="product-list">
<view class="product-management">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">商品管理</text>
<text class="page-subtitle">管理系统商品信息和库存</text>
</view>
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ 'active': activeTab === tab.key }"
@click="switchTab(tab.key)"
>
<text class="iconfont tab-icon">{{ tab.icon }}</text>
<text class="tab-title">{{ tab.title }}</text>
</view>
</view>
<!-- 商品列表Tab -->
<view v-if="activeTab === 'product-list'">
<!-- 统计卡片 -->
<view class="stats-cards">
<view class="stat-card">
<view class="stat-icon">📦</view>
<view class="stat-content">
<text class="stat-value">{{ totalProducts }}</text>
<text class="stat-label">总商品数</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">✅</view>
<view class="stat-content">
<text class="stat-value">{{ onSaleProducts }}</text>
<text class="stat-label">在售商品</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">🚫</view>
<view class="stat-content">
<text class="stat-value">{{ offShelfProducts }}</text>
<text class="stat-label">已下架</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">⚠️</view>
<view class="stat-content">
<text class="stat-value">{{ lowStockProducts }}</text>
<text class="stat-label">库存不足</text>
</view>
</view>
</view>
<!-- 搜索和筛选 -->
<view class="search-section">
<view class="search-bar">
<view class="search-input-wrapper">
<input
v-model="searchKeyword"
class="search-input"
placeholder="搜索商品名称、编号、分类"
@confirm="handleSearch"
/>
<view class="search-btn" @click="handleSearch">
<text class="iconfont icon-search"></text>
</view>
</view>
<view class="advanced-toggle" @click="showAdvancedSearch = !showAdvancedSearch">
<text>{{ showAdvancedSearch ? '收起' : '展开' }}筛选</text>
<text class="iconfont">{{ showAdvancedSearch ? 'icon-up' : 'icon-down' }}</text>
</view>
</view>
<!-- 高级搜索 -->
<view v-if="showAdvancedSearch" class="advanced-search">
<view class="filter-row">
<view class="filter-item">
<text class="filter-label">商品状态:</text>
<picker
mode="selector"
:range="statusOptions"
:value="selectedStatus"
@change="handleStatusChange"
>
<view class="picker-display">
<text>{{ statusOptions[selectedStatus] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">商品分类:</text>
<picker
mode="selector"
:range="categoryOptions"
:value="selectedCategory"
@change="handleCategoryChange"
>
<view class="picker-display">
<text>{{ categoryOptions[selectedCategory] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
</view>
<view class="filter-row">
<view class="filter-item">
<text class="filter-label">价格区间:</text>
<view class="price-range">
<input
v-model="minPrice"
class="price-input"
placeholder="最低价"
type="number"
/>
<text class="price-separator">-</text>
<input
v-model="maxPrice"
class="price-input"
placeholder="最高价"
type="number"
/>
</view>
</view>
</view>
<view class="filter-actions">
<button class="btn-secondary" @click="resetFilters">重置</button>
<button class="btn-primary" @click="handleAdvancedSearch">搜索</button>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<view class="left-actions">
<button class="btn-primary" @click="showAddModal = true">
<text class="iconfont icon-add"></text>
添加商品
</button>
<button class="btn-secondary" @click="handleBatchOnSale">
<text class="iconfont icon-onsale"></text>
批量上架
</button>
<button class="btn-warning" @click="handleBatchOffShelf">
<text class="iconfont icon-offshelf"></text>
批量下架
</button>
</view>
<view class="right-actions">
<view class="select-all">
<checkbox :checked="selectAll" @change="handleSelectAll" />
<text>全选</text>
</view>
<button class="btn-danger" :disabled="selectedProducts.length === 0" @click="handleBatchDelete">
<text class="iconfont icon-delete"></text>
批量删除
</button>
</view>
</view>
<!-- 商品列表 -->
<view class="product-table">
<view class="table-header">
<view class="table-row">
<view class="col-select">选择</view>
<view class="col-image">商品图片</view>
<view class="col-info">商品信息</view>
<view class="col-price">价格</view>
<view class="col-stock">库存</view>
<view class="col-status">状态</view>
<view class="col-sales">销量</view>
<view class="col-actions">操作</view>
</view>
</view>
<view class="table-body">
<view
v-for="product in productList"
:key="product.id"
class="table-row"
:class="{ 'selected': selectedProducts.includes(product.id) }"
>
<view class="col-select">
<checkbox
:checked="selectedProducts.includes(product.id)"
@change="handleProductSelect(product.id, $event)"
/>
</view>
<view class="col-image">
<image :src="product.image" class="product-image" />
</view>
<view class="col-info">
<view class="product-name">{{ product.name }}</view>
<view class="product-code">{{ product.code }}</view>
<view class="product-category">{{ product.category }}</view>
</view>
<view class="col-price">
<view class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
</view>
<view class="current-price">¥{{ product.price }}</view>
</view>
<view class="col-stock">
<view class="stock-text" :class="{ 'low-stock': product.stock < 10 }">
{{ product.stock }}
</view>
</view>
<view class="col-status">
<view class="status-badge" :class="product.status">
{{ product.status === 'on_sale' ? '在售' : product.status === 'off_shelf' ? '下架' : '待审核' }}
</view>
</view>
<view class="col-sales">
<text class="sales-text">{{ product.sales }}</text>
</view>
<view class="col-actions">
<button class="action-btn edit" @click="handleEdit(product)">
<text class="iconfont icon-edit"></text>
</button>
<button class="action-btn onsale" @click="handleOnSale(product)" v-if="product.status === 'off_shelf'">
<text class="iconfont icon-onsale"></text>
</button>
<button class="action-btn offshelf" @click="handleOffShelf(product)" v-else-if="product.status === 'on_sale'">
<text class="iconfont icon-offshelf"></text>
</button>
<button class="action-btn delete" @click="handleDelete(product)">
<text class="iconfont icon-delete"></text>
</button>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination">
<view class="page-info">
<text>共 {{ totalProducts }} 条记录,显示 {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, totalProducts) }} 条</text>
</view>
<view class="page-controls">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
上一页
</button>
<view class="page-numbers">
<button
v-for="page in visiblePages"
:key="page"
class="page-number"
:class="{ 'active': page === currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
</view>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
下一页
</button>
</view>
</view>
<!-- 添加/编辑商品模态框 -->
<view v-if="showAddModal || showEditModal" class="modal-overlay" @click="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ showAddModal ? '添加商品' : '编辑商品' }}</text>
<view class="modal-close" @click="closeModal">
<text class="iconfont icon-close"></text>
</view>
</view>
<view class="modal-body">
<view class="form-row">
<view class="form-group">
<text class="form-label">商品名称</text>
<input
v-model="productForm.name"
class="form-input"
placeholder="请输入商品名称"
/>
</view>
<view class="form-group">
<text class="form-label">商品编号</text>
<input
v-model="productForm.code"
class="form-input"
placeholder="请输入商品编号"
/>
</view>
</view>
<view class="form-row">
<view class="form-group">
<text class="form-label">商品分类</text>
<picker
mode="selector"
:range="categoryOptions"
:value="productForm.category"
@change="handleFormCategoryChange"
>
<view class="form-select">
<text>{{ categoryOptions[productForm.category] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
<view class="form-group">
<text class="form-label">商品状态</text>
<picker
mode="selector"
:range="statusOptions"
:value="productForm.status"
@change="handleFormStatusChange"
>
<view class="form-select">
<text>{{ statusOptions[productForm.status] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
</view>
<view class="form-row">
<view class="form-group">
<text class="form-label">原价</text>
<input
v-model="productForm.originalPrice"
class="form-input"
placeholder="请输入原价"
type="number"
/>
</view>
<view class="form-group">
<text class="form-label">现价</text>
<input
v-model="productForm.price"
class="form-input"
placeholder="请输入现价"
type="number"
/>
</view>
<view class="form-group">
<text class="form-label">库存</text>
<input
v-model="productForm.stock"
class="form-input"
placeholder="请输入库存数量"
type="number"
/>
</view>
</view>
<view class="form-group">
<text class="form-label">商品描述</text>
<textarea
v-model="productForm.description"
class="form-textarea"
placeholder="请输入商品描述"
:maxlength="500"
/>
</view>
<view class="form-group">
<text class="form-label">商品图片</text>
<view class="image-upload">
<view class="upload-area" @click="handleImageUpload">
<text class="iconfont icon-upload"></text>
<text class="upload-text">点击上传图片</text>
</view>
<view v-if="productForm.image" class="image-preview">
<image :src="productForm.image" class="preview-image" />
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-secondary" @click="closeModal">取消</button>
<button class="btn-primary" @click="handleSaveProduct">
{{ showAddModal ? '添加' : '保存' }}
</button>
</view>
</view>
</view>
<!-- 删除确认模态框 -->
<view v-if="showDeleteModal" class="modal-overlay" @click="closeModal">
<view class="modal-content delete-modal" @click.stop>
<view class="modal-header">
<text class="modal-title">确认删除</text>
</view>
<view class="modal-body">
<text class="delete-text">
确定要删除商品 "{{ deleteProduct?.name }}" 吗?此操作不可恢复。
</text>
</view>
<view class="modal-footer">
<button class="btn-secondary" @click="closeModal">取消</button>
<button class="btn-danger" @click="confirmDelete">确认删除</button>
</view>
</view>
</view>
<!-- 商品分类Tab -->
<view v-if="activeTab === 'category'" class="category-section">
<view class="category-management">
<view class="category-header">
<text class="section-title">商品分类管理</text>
<button class="btn-primary" @click="showCategoryModal = true">
<text class="iconfont icon-add"></text>
添加分类
</button>
</view>
<view class="category-tree">
<view class="category-item" v-for="category in categories" :key="category.id">
<view class="category-info">
<text class="category-name">{{ category.name }}</text>
<text class="category-count">({{ category.productCount }}商品)</text>
</view>
<view class="category-actions">
<button class="action-btn edit" @click="editCategory(category)">
<text class="iconfont icon-edit"></text>
</button>
<button class="action-btn delete" @click="deleteCategory(category)">
<text class="iconfont icon-delete"></text>
</button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref, computed } from 'vue'
import AdminLayout from '@/layouts/admin/index.uvue'
// Tab 相关
const activeTab = ref('product-list')
const tabs = ref([
{ key: 'product-list', title: '商品列表', icon: 'icon-list' },
{ key: 'product-add', title: '添加商品', icon: 'icon-add' },
{ key: 'category', title: '商品分类', icon: 'icon-category' }
])
// 响应式数据
const searchKeyword = ref('')
const showAdvancedSearch = ref(false)
const totalProducts = ref(856)
const onSaleProducts = ref(789)
const offShelfProducts = ref(45)
const lowStockProducts = ref(22)
// 筛选条件
const selectedStatus = ref(0)
const selectedCategory = ref(0)
const minPrice = ref('')
const maxPrice = ref('')
// 选项数据
const statusOptions = ['全部状态', '在售', '下架', '待审核']
const categoryOptions = ['全部分类', '电子产品', '服装鞋帽', '食品饮料', '家居用品', '图书文具']
// 商品列表
const productList = ref([
{
id: 1,
name: 'iPhone 15 Pro',
code: 'IP15P001',
category: '电子产品',
price: 8999,
originalPrice: 9999,
stock: 50,
status: 'on_sale',
sales: 1250,
image: '/static/products/iphone15.jpg',
description: '最新款 iPhone 15 Pro性能卓越'
},
{
id: 2,
name: 'Nike 运动鞋',
code: 'NK001',
category: '服装鞋帽',
price: 599,
originalPrice: 699,
stock: 120,
status: 'on_sale',
sales: 890,
image: '/static/products/nike.jpg',
description: '舒适透气的运动鞋'
},
{
id: 3,
name: '联想笔记本',
code: 'LN001',
category: '电子产品',
price: 4599,
originalPrice: 4999,
stock: 3,
status: 'on_sale',
sales: 456,
image: '/static/products/lenovo.jpg',
description: '高性能商务笔记本'
},
{
id: 4,
name: '咖啡机',
code: 'CF001',
category: '家居用品',
price: 1299,
originalPrice: 1599,
stock: 25,
status: 'off_shelf',
sales: 234,
image: '/static/products/coffee.jpg',
description: '全自动咖啡机'
}
])
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const totalPages = computed(() => Math.ceil(totalProducts.value / pageSize.value))
const visiblePages = computed(() => {
const pages = []
const start = Math.max(1, currentPage.value - 2)
const end = Math.min(totalPages.value, currentPage.value + 2)
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
})
// 选择相关
const selectAll = ref(false)
const selectedProducts = ref<number[]>([])
// 模态框相关
const showAddModal = ref(false)
const showEditModal = ref(false)
const showDeleteModal = ref(false)
const deleteProduct = ref(null)
// 分类数据
const categories = ref([
{ id: 1, name: '电子产品', productCount: 156 },
{ id: 2, name: '服装鞋帽', productCount: 89 },
{ id: 3, name: '食品饮料', productCount: 67 },
{ id: 4, name: '家居用品', productCount: 45 },
{ id: 5, name: '图书文具', productCount: 34 }
])
const showCategoryModal = ref(false)
// 表单数据
const productForm = ref({
id: 0,
name: '',
code: '',
category: 0,
price: '',
originalPrice: '',
stock: '',
status: 0,
description: '',
image: ''
})
// 方法
const handleSearch = () => {
console.log('搜索:', searchKeyword.value)
// TODO: 实现搜索逻辑
}
const handleStatusChange = (e: any) => {
selectedStatus.value = e.detail.value
}
const handleCategoryChange = (e: any) => {
selectedCategory.value = e.detail.value
}
const handleFormCategoryChange = (e: any) => {
productForm.value.category = e.detail.value
}
const handleFormStatusChange = (e: any) => {
productForm.value.status = e.detail.value
}
const resetFilters = () => {
selectedStatus.value = 0
selectedCategory.value = 0
minPrice.value = ''
maxPrice.value = ''
searchKeyword.value = ''
}
const handleAdvancedSearch = () => {
console.log('高级搜索:', {
status: statusOptions[selectedStatus.value],
category: categoryOptions[selectedCategory.value],
minPrice: minPrice.value,
maxPrice: maxPrice.value,
keyword: searchKeyword.value
})
// TODO: 实现高级搜索逻辑
}
const handleSelectAll = (e: any) => {
selectAll.value = e.detail.value
if (selectAll.value) {
selectedProducts.value = productList.value.map(product => product.id)
} else {
selectedProducts.value = []
}
}
const handleProductSelect = (productId: number, e: any) => {
if (e.detail.value) {
selectedProducts.value.push(productId)
} else {
selectedProducts.value = selectedProducts.value.filter(id => id !== productId)
}
selectAll.value = selectedProducts.value.length === productList.value.length
}
const handleBatchOnSale = () => {
if (selectedProducts.value.length === 0) return
uni.showModal({
title: '确认上架',
content: `确定要上架 ${selectedProducts.value.length} 个商品吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现批量上架逻辑
selectedProducts.value.forEach(id => {
const product = productList.value.find(p => p.id === id)
if (product) product.status = 'on_sale'
})
uni.showToast({
title: '批量上架成功',
icon: 'success'
})
selectedProducts.value = []
selectAll.value = false
}
}
})
}
const handleBatchOffShelf = () => {
if (selectedProducts.value.length === 0) return
uni.showModal({
title: '确认下架',
content: `确定要下架 ${selectedProducts.value.length} 个商品吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现批量下架逻辑
selectedProducts.value.forEach(id => {
const product = productList.value.find(p => p.id === id)
if (product) product.status = 'off_shelf'
})
uni.showToast({
title: '批量下架成功',
icon: 'success'
})
selectedProducts.value = []
selectAll.value = false
}
}
})
}
const handleBatchDelete = () => {
if (selectedProducts.value.length === 0) return
uni.showModal({
title: '确认删除',
content: `确定要删除 ${selectedProducts.value.length} 个商品吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现批量删除逻辑
selectedProducts.value.forEach(id => {
const index = productList.value.findIndex(p => p.id === id)
if (index > -1) {
productList.value.splice(index, 1)
totalProducts.value--
}
})
uni.showToast({
title: '批量删除成功',
icon: 'success'
})
selectedProducts.value = []
selectAll.value = false
}
}
})
}
const handleEdit = (product: any) => {
productForm.value = {
id: product.id,
name: product.name,
code: product.code,
category: categoryOptions.indexOf(product.category),
price: product.price.toString(),
originalPrice: product.originalPrice.toString(),
stock: product.stock.toString(),
status: statusOptions.indexOf(product.status === 'on_sale' ? '在售' : product.status === 'off_shelf' ? '下架' : '待审核'),
description: product.description,
image: product.image
}
showEditModal.value = true
}
const handleOnSale = (product: any) => {
uni.showModal({
title: '确认上架',
content: `确定要上架商品 "${product.name}" 吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现上架逻辑
product.status = 'on_sale'
uni.showToast({
title: '上架成功',
icon: 'success'
})
}
}
})
}
const handleOffShelf = (product: any) => {
uni.showModal({
title: '确认下架',
content: `确定要下架商品 "${product.name}" 吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现下架逻辑
product.status = 'off_shelf'
uni.showToast({
title: '下架成功',
icon: 'success'
})
}
}
})
}
const handleDelete = (product: any) => {
deleteProduct.value = product
showDeleteModal.value = true
}
const confirmDelete = () => {
// TODO: 实现删除逻辑
const index = productList.value.findIndex(p => p.id === deleteProduct.value.id)
if (index > -1) {
productList.value.splice(index, 1)
totalProducts.value--
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
closeModal()
}
const handleSaveProduct = () => {
// 表单验证
if (!productForm.value.name || !productForm.value.code) {
uni.showToast({
title: '请填写必填信息',
icon: 'none'
})
return
}
if (showAddModal.value) {
// TODO: 实现添加商品逻辑
const newProduct = {
id: Date.now(),
name: productForm.value.name,
code: productForm.value.code,
category: categoryOptions[productForm.value.category],
price: parseFloat(productForm.value.price),
originalPrice: parseFloat(productForm.value.originalPrice),
stock: parseInt(productForm.value.stock),
status: statusOptions[productForm.value.status] === '在售' ? 'on_sale' : statusOptions[productForm.value.status] === '下架' ? 'off_shelf' : 'pending',
sales: 0,
image: productForm.value.image || '/static/products/default.jpg',
description: productForm.value.description
}
productList.value.unshift(newProduct)
totalProducts.value++
uni.showToast({
title: '添加成功',
icon: 'success'
})
} else {
// TODO: 实现编辑商品逻辑
const product = productList.value.find(p => p.id === productForm.value.id)
if (product) {
product.name = productForm.value.name
product.code = productForm.value.code
product.category = categoryOptions[productForm.value.category]
product.price = parseFloat(productForm.value.price)
product.originalPrice = parseFloat(productForm.value.originalPrice)
product.stock = parseInt(productForm.value.stock)
product.status = statusOptions[productForm.value.status] === '在售' ? 'on_sale' : statusOptions[productForm.value.status] === '下架' ? 'off_shelf' : 'pending'
product.description = productForm.value.description
product.image = productForm.value.image
}
uni.showToast({
title: '保存成功',
icon: 'success'
})
}
closeModal()
}
const handleImageUpload = () => {
uni.showToast({
title: '图片上传功能开发中',
icon: 'none'
})
}
const closeModal = () => {
showAddModal.value = false
showEditModal.value = false
showDeleteModal.value = false
deleteProduct.value = null
// 重置表单
productForm.value = {
id: 0,
name: '',
code: '',
category: 0,
price: '',
originalPrice: '',
stock: '',
status: 0,
description: '',
image: ''
}
}
const goToPage = (page: number) => {
currentPage.value = page
// TODO: 实现分页数据加载
}
// Tab 切换方法
const switchTab = (tabKey: string) => {
activeTab.value = tabKey
// 根据不同tab切换显示内容
if (tabKey === 'product-add') {
showAddModal.value = true
} else {
showAddModal.value = false
}
}
// 分类管理方法
const editCategory = (category: any) => {
uni.showToast({
title: '编辑分类功能开发中',
icon: 'none'
})
}
const deleteCategory = (category: any) => {
uni.showModal({
title: '确认删除',
content: `确定要删除分类 "${category.name}" 吗?`,
success: (res) => {
if (res.confirm) {
const index = categories.value.findIndex(c => c.id === category.id)
if (index > -1) {
categories.value.splice(index, 1)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
}
// 页面生命周期
onLoad((options: any) => {
// 处理页面参数切换到对应的tab
if (options && options.action) {
if (options.action === 'add') {
activeTab.value = 'product-add'
showAddModal.value = true
} else {
activeTab.value = 'product-list'
}
} else if (options && options.tab) {
if (options.tab === 'category') {
activeTab.value = 'category'
} else {
activeTab.value = 'product-list'
}
} else {
activeTab.value = 'product-list'
}
console.log('商品管理页面加载,参数:', options)
})
onMounted(() => {
// 初始化数据
console.log('商品管理页面初始化')
})
</script>
<style lang="scss">
/* Tab 栏样式 */
.tab-bar {
display: flex;
background-color: #ffffff;
border-radius: 8rpx;
padding: 8rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 16rpx 24rpx;
border-radius: 6rpx;
cursor: pointer;
transition: all 0.2s;
background-color: #f5f5f5;
color: #666666;
}
.tab-item:hover {
background-color: #e8e8e8;
}
.tab-item.active {
background-color: #1890ff;
color: #ffffff;
}
.tab-icon {
font-size: 16rpx;
}
.tab-title {
font-size: 14rpx;
font-weight: 500;
}
/* 分类管理样式 */
.category-section {
background-color: #ffffff;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.category-management {
padding: 32rpx;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
.section-title {
font-size: 18rpx;
font-weight: 600;
color: #262626;
}
.category-tree {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.category-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background-color: #fafafa;
border-radius: 6rpx;
border: 1rpx solid #e8e8e8;
}
.category-info {
display: flex;
align-items: center;
gap: 12rpx;
}
.category-name {
font-size: 16rpx;
font-weight: 500;
color: #262626;
}
.category-count {
font-size: 14rpx;
color: #666666;
}
.category-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
width: 40rpx;
height: 40rpx;
border-radius: 6rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
font-size: 16rpx;
}
.action-btn.edit {
background-color: #1890ff;
color: #ffffff;
}
.action-btn.delete {
background-color: #ff4d4f;
color: #ffffff;
}
.product-management {
padding: 20rpx;
}
.page-header {
margin-bottom: 30rpx;
}
.page-title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.page-subtitle {
display: block;
font-size: 26rpx;
color: #666;
}
.stats-cards {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
flex-wrap: wrap;
gap: 20rpx;
}
.stat-card {
flex: 1;
min-width: 200rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16rpx;
padding: 40rpx 30rpx;
text-align: center;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.stat-icon {
font-size: 48rpx;
margin-bottom: 20rpx;
}
.stat-content {
flex: 1;
}
.stat-value {
display: block;
font-size: 48rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
opacity: 0.9;
}
.search-section {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.search-bar {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 20rpx;
}
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
border: 1rpx solid #ddd;
border-radius: 8rpx;
overflow: hidden;
}
.search-input {
flex: 1;
padding: 16rpx 20rpx;
border: none;
font-size: 28rpx;
}
.search-btn {
padding: 16rpx 20rpx;
background-color: #1890ff;
color: #fff;
border: none;
cursor: pointer;
}
.advanced-toggle {
display: flex;
align-items: center;
gap: 10rpx;
color: #1890ff;
cursor: pointer;
font-size: 26rpx;
}
.advanced-search {
border-top: 1rpx solid #eee;
padding-top: 30rpx;
margin-top: 20rpx;
}
.filter-row {
display: flex;
gap: 30rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.filter-item {
flex: 1;
min-width: 200rpx;
}
.filter-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.picker-display {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
background-color: #fff;
cursor: pointer;
}
.price-range {
display: flex;
align-items: center;
gap: 10rpx;
}
.price-input {
flex: 1;
padding: 16rpx 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
}
.price-separator {
color: #666;
font-size: 24rpx;
}
.filter-actions {
display: flex;
gap: 20rpx;
justify-content: flex-end;
margin-top: 20rpx;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
flex-wrap: wrap;
gap: 20rpx;
}
.left-actions,
.right-actions {
display: flex;
align-items: center;
gap: 15rpx;
}
.select-all {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 26rpx;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 16rpx 24rpx;
font-size: 26rpx;
cursor: pointer;
display: flex;
align-items: center;
gap: 8rpx;
}
.btn-secondary {
background-color: #fff;
color: #666;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 16rpx 24rpx;
font-size: 26rpx;
cursor: pointer;
display: flex;
align-items: center;
gap: 8rpx;
}
.btn-warning {
background-color: #faad14;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 16rpx 24rpx;
font-size: 26rpx;
cursor: pointer;
display: flex;
align-items: center;
gap: 8rpx;
}
.btn-danger {
background-color: #ff4d4f;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 16rpx 24rpx;
font-size: 26rpx;
cursor: pointer;
display: flex;
align-items: center;
gap: 8rpx;
}
.btn-danger:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.product-table {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.table-header {
background-color: #f5f5f5;
border-bottom: 1rpx solid #eee;
}
.table-row {
display: flex;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.table-row:last-child {
border-bottom: none;
}
.table-row.selected {
background-color: #f0f8ff;
}
.col-select {
width: 80rpx;
text-align: center;
}
.col-image {
width: 120rpx;
text-align: center;
}
.col-info {
flex: 2;
}
.col-price,
.col-stock,
.col-sales {
flex: 1;
}
.col-status {
flex: 1.2;
}
.col-actions {
width: 250rpx;
display: flex;
gap: 10rpx;
justify-content: center;
}
.product-image {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
object-fit: cover;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.product-code {
font-size: 24rpx;
color: #999;
margin-bottom: 4rpx;
}
.product-category {
font-size: 24rpx;
color: #666;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
margin-bottom: 4rpx;
}
.current-price {
font-size: 32rpx;
font-weight: bold;
color: #ff4d4f;
}
.stock-text {
font-size: 26rpx;
color: #333;
}
.stock-text.low-stock {
color: #ff4d4f;
font-weight: bold;
}
.status-badge {
display: inline-block;
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 22rpx;
font-weight: bold;
text-align: center;
}
.status-badge.on_sale {
background-color: #52c41a;
color: #fff;
}
.status-badge.off_shelf {
background-color: #ff4d4f;
color: #fff;
}
.status-badge.pending {
background-color: #faad14;
color: #fff;
}
.sales-text {
font-size: 26rpx;
color: #666;
}
.action-btn {
width: 60rpx;
height: 60rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
font-size: 24rpx;
}
.action-btn.edit {
background-color: #1890ff;
color: #fff;
}
.action-btn.onsale {
background-color: #52c41a;
color: #fff;
}
.action-btn.offshelf {
background-color: #faad14;
color: #fff;
}
.action-btn.delete {
background-color: #ff4d4f;
color: #fff;
}
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30rpx;
flex-wrap: wrap;
gap: 20rpx;
}
.page-info {
font-size: 26rpx;
color: #666;
}
.page-controls {
display: flex;
align-items: center;
gap: 15rpx;
}
.page-btn {
padding: 12rpx 20rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
background-color: #fff;
color: #333;
cursor: pointer;
font-size: 26rpx;
}
.page-btn:disabled {
background-color: #f5f5f5;
color: #ccc;
cursor: not-allowed;
}
.page-numbers {
display: flex;
gap: 8rpx;
margin: 0 20rpx;
}
.page-number {
width: 50rpx;
height: 50rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
background-color: #fff;
color: #333;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 26rpx;
}
.page-number.active {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
border-radius: 12rpx;
width: 90%;
max-width: 700rpx;
max-height: 80vh;
overflow-y: auto;
}
.delete-modal {
max-width: 400rpx;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close {
cursor: pointer;
font-size: 28rpx;
color: #999;
padding: 10rpx;
}
.modal-body {
padding: 30rpx;
}
.form-row {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.form-group {
flex: 1;
min-width: 200rpx;
}
.form-label {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 10rpx;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 16rpx 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.form-textarea {
min-height: 120rpx;
resize: vertical;
}
.form-select {
padding: 16rpx 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
background-color: #fff;
cursor: pointer;
font-size: 28rpx;
}
.image-upload {
display: flex;
align-items: center;
gap: 20rpx;
}
.upload-area {
width: 120rpx;
height: 120rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
color: #999;
}
.upload-text {
font-size: 24rpx;
margin-top: 8rpx;
}
.preview-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
object-fit: cover;
}
.modal-footer {
display: flex;
gap: 20rpx;
justify-content: flex-end;
padding: 30rpx;
border-top: 1rpx solid #eee;
}
.delete-text {
font-size: 28rpx;
color: #333;
text-align: center;
line-height: 1.5;
}
.iconfont {
font-family: 'iconfont';
font-size: 24rpx;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.stats-cards {
flex-direction: column;
}
.filter-row {
flex-direction: column;
gap: 15rpx;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.left-actions,
.right-actions {
justify-content: center;
}
.table-row {
flex-wrap: wrap;
gap: 10rpx;
}
.col-info {
flex: 1 1 100%;
}
.form-row {
flex-direction: column;
gap: 15rpx;
}
.pagination {
flex-direction: column;
text-align: center;
}
}
</style>