Files
medical-mall/pages/mall/admin/product/product-management/index.uvue
2026-03-09 17:57:44 +08:00

566 lines
15 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>
<view class="product-list-page">
<!-- 1. 搜索表单 -->
<view class="search-card">
<view class="search-row">
<view class="search-item">
<text class="label">商品搜索:</text>
<input class="mock-input" placeholder="请输入商品名称/关键字/ID" />
</view>
<view class="search-item">
<text class="label">商品类型:</text>
<view class="mock-select">
<text>全部</text>
<text class="arrow">▼</text>
</view>
</view>
<view class="search-item">
<text class="label">商品分类:</text>
<view class="mock-select">
<text>请选择</text>
<text class="arrow">▼</text>
</view>
</view>
<view class="search-btns">
<button class="btn-primary">查询</button>
<button class="btn-reset">重置</button>
<view class="expand-control">
<text class="expand-txt">展开</text>
<text class="expand-arrow">▼</text>
</view>
</view>
</view>
<view class="search-row mt-12">
<view class="search-item">
<text class="label">配送方式:</text>
<view class="mock-select">
<text>全部</text>
<text class="arrow">▼</text>
</view>
</view>
</view>
</view>
<!-- 2. 商品状态 Tabs -->
<view class="status-tabs-wrap">
<view class="status-tabs">
<view
v-for="(tab, index) in statusTabs"
:key="index"
class="tab-item"
:class="{ active: activeStatus === tab.key }"
@click="activeStatus = tab.key"
>
<text>{{ tab.label }}({{ tab.count }})</text>
</view>
</view>
</view>
<!-- 3. 操作按钮行 -->
<view class="action-bar">
<view class="left-actions">
<button class="btn-add" @click="goEdit(null)">添加商品</button>
<button class="btn-collect">商品采集</button>
<view class="btn-dropdown">
<text>批量修改</text>
<text class="arrow">▼</text>
</view>
<view class="btn-dropdown">
<text>商品迁移</text>
<text class="arrow">▼</text>
</view>
<button class="btn-export">数据导出</button>
</view>
</view>
<!-- 4. 商品列表表格 -->
<view class="list-card">
<view class="table-v5">
<view class="th-row">
<view class="th col-check"><text>□</text></view>
<view class="th col-id"><text>商品ID</text></view>
<view class="th col-img"><text>商品图</text></view>
<view class="th col-name"><text>商品名称</text></view>
<view class="th col-activity"><text>参与活动</text></view>
<view class="th col-type"><text>商品类型</text></view>
<view class="th col-price"><text>商品售价</text></view>
<view class="th col-sales"><text>销量</text></view>
<view class="th col-stock"><text>库存</text></view>
<view class="th col-sort"><text>排序</text></view>
<view class="th col-status"><text>状态</text></view>
<view class="th col-op"><text>操作</text></view>
</view>
<view v-for="(item, index) in productList" :key="index"
class="tr-row"
:style="{ zIndex: activeDropdownId === item.id ? 1000 : 1 }"
>
<view class="td col-check"><text>□</text></view>
<view class="td col-id"><text>{{ item.id }}</text></view>
<view class="td col-img">
<image class="p-img" :src="item.image" mode="aspectFill" />
</view>
<view class="td col-name">
<text class="p-name-txt">{{ item.name }}</text>
</view>
<view class="td col-activity">
<view class="activity-tags">
<text v-for="tag in item.activities" :key="tag" class="tag" :class="tag">{{ getActivityName(tag) }}</text>
<text v-if="item.activities.length === 0">-</text>
</view>
</view>
<view class="td col-type"><text>{{ item.typeName }}</text></view>
<view class="td col-price"><text>{{ item.price }}</text></view>
<view class="td col-sales"><text>{{ item.sales }}</text></view>
<view class="td col-stock"><text>{{ item.stock }}</text></view>
<view class="td col-sort"><text>{{ item.sort }}</text></view>
<view class="td col-status">
<StatusSwitch
:modelValue="item.status === 1"
activeText="上架"
inactiveText="下架"
@update:modelValue="(val : boolean) => item.status = (val ? 1 : 0)"
/>
</view>
<view class="td col-op op-group">
<text class="op-link" @click.stop="goEdit(item.id)">编辑</text>
<text class="op-divider">|</text>
<view class="more-hover-box"
@mouseenter="activeDropdownId = item.id"
@mouseleave="activeDropdownId = null"
>
<view class="more-trigger-txt">
<text class="more-link-text">更多</text>
<text class="more-arrow-icon"></text>
</view>
<view class="dropdown-list-container" v-show="activeDropdownId === item.id">
<view class="dropdown-triangle"></view>
<view class="dropdown-menu-body">
<text class="menu-item" @click.stop="goReviews(item.id)">查看评论</text>
<text class="menu-item" @click.stop="goMemberPrice(item.id)">会员价管理</text>
<text class="menu-item" @click.stop="uni.showToast({title:'佣金管理开发中', icon:'none'})">佣金管理</text>
<text class="menu-item danger-item" @click.stop="moveToRecycle(item.id)">{{ activeStatus === 'recycle' ? '恢复商品' : '移到回收站' }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 5. 分页 -->
<view class="pagination-row">
<text class="total">共 {{ total }} 条</text>
<view class="page-ctrl">
<text class="page-btn disabled">{"<"}</text>
<text class="page-num active">1</text>
<text class="page-num">2</text>
<text class="page-btn">{">"}</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
import StatusSwitch from '@/components/StatusSwitch.uvue'
const total = ref(49)
const activeStatus = ref('selling')
const activeDropdownId = ref<number | null>(null)
const statusTabs = ref([
{ key: 'selling', label: '出售中的商品', count: 49 },
{ key: 'warehouse', label: '仓库中的商品', count: 4 },
{ key: 'soldout', label: '已经售罄商品', count: 11 },
{ key: 'alarm', label: '警戒库存商品', count: 27 },
{ key: 'recycle', label: '回收站的商品', count: 176 },
])
const productList = ref([
{
id: 963,
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
activities: ['kj', 'pt'],
typeName: '普通商品',
price: '0.01',
sales: 639,
stock: 1602,
sort: 9999,
status: 1
},
{
id: 108,
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
name: 'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师师单椅单沙头层牛皮/单椅',
activities: ['pt', 'ms'],
typeName: '普通商品',
price: '7580.00',
sales: 14,
stock: 16638,
sort: 9999,
status: 1
},
{
id: 48,
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水蓝/传奇墨水蓝/白 XL',
activities: ['kj', 'pt', 'ms'],
typeName: '普通商品',
price: '100.00',
sales: 841,
stock: 2318,
sort: 9998,
status: 1
}
])
function getActivityName(tag: string): string {
if (tag === 'kj') return '砍价'
if (tag === 'pt') return '拼团'
if (tag === 'ms') return '秒杀'
return tag
}
function goEdit(id: number | null) {
openRoute('product_edit')
}
function goReviews(id: number) {
openRoute('product_productReply')
}
function goMemberPrice(id: number) {
openRoute('product_member_price')
}
function moveToRecycle(id: number) {
const action = activeStatus.value === 'recycle' ? '恢复' : '移到回收站';
uni.showModal({
title: '提示',
content: `确认要将该商品${action}吗?`,
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '操作成功', icon: 'success' });
}
}
})
}
</script>
<style scoped lang="scss">
.product-list-page {
/* 使用 Layout 的背景和内边距 */
min-height: auto;
}
.search-card {
background: #fff;
padding: var(--admin-card-padding);
border-radius: 4px;
margin-bottom: var(--admin-section-gap);
}
.search-row {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 24px;
}
.mt-12 { margin-top: 12px; }
.search-item {
display: flex;
flex-direction: row;
align-items: center;
.label { font-size: 14px; color: #606266; width: 80px; text-align: right; }
}
.mock-input {
width: 200px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
}
.mock-select {
width: 160px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 13px;
color: #606266;
.arrow { font-size: 10px; color: #c0c4cc; }
}
.search-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.btn-primary { background: #1890ff; color: #fff; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 13px; border: none; }
.btn-reset { background: #fff; color: #606266; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 13px; border: 1px solid #dcdfe6; }
.expand-control { display: flex; flex-direction: row; align-items: center; gap: 4px; cursor: pointer; }
.expand-txt { font-size: 13px; color: #1890ff; }
.expand-arrow { font-size: 10px; color: #1890ff; }
.status-tabs-wrap {
margin-bottom: 20px;
}
.status-tabs {
display: flex;
flex-direction: row;
border-bottom: 2px solid #f0f2f5;
}
.tab-item {
padding: 12px 20px;
font-size: 14px;
color: #606266;
position: relative;
cursor: pointer;
&.active {
color: #1890ff;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #1890ff;
}
}
}
.action-bar {
margin-bottom: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.left-actions {
display: flex;
flex-direction: row;
gap: 12px;
align-items: center;
}
.btn-add { background: #1890ff; color: #fff; height: 32px; padding: 0 12px; border-radius: 4px; font-size: 13px; border: none; }
.btn-collect { background: #13ce66; color: #fff; height: 32px; padding: 0 12px; border-radius: 4px; font-size: 13px; border: none; }
.btn-export { background: #fff; color: #606266; height: 32px; padding: 0 12px; border-radius: 4px; font-size: 13px; border: 1px solid #dcdfe6; }
.btn-dropdown {
height: 32px;
padding: 0 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
font-size: 13px;
color: #606266;
.arrow { font-size: 10px; color: #c4cc; }
}
.list-card {
background: #fff;
border-radius: 4px;
padding-bottom: 20px;
position: relative;
overflow: visible;
}
.table-v5 {
width: 100%;
overflow: visible;
}
.th-row {
display: flex;
flex-direction: row;
background-color: #f2f2f2;
}
.th {
padding: 12px 8px;
font-size: 13px;
font-weight: bold;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.tr-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
position: relative;
overflow: visible;
&:hover { background-color: #fafafa; }
}
.td {
padding: 12px 8px;
font-size: 13px;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
}
.col-check { width: 50px; }
.col-id { width: 80px; }
.col-img { width: 80px; }
.col-name { flex: 1; justify-content: flex-start; text-align: left; }
.col-activity { width: 140px; }
.col-type { width: 100px; }
.col-price { width: 100px; }
.col-sales { width: 80px; }
.col-stock { width: 80px; }
.col-sort { width: 80px; }
.col-status { width: 100px; }
.col-op { width: 150px; }
.op-group {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.more-hover-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 40px;
padding: 0 10px;
cursor: pointer;
}
.more-trigger-txt {
display: flex;
flex-direction: row;
align-items: center;
pointer-events: none; /* 让事件透传给 more-hover-box */
}
.more-link-text {
font-size: 13px;
color: #1890ff;
}
.more-arrow-icon {
font-size: 10px;
color: #1890ff;
margin-left: 2px;
}
.dropdown-list-container {
position: absolute;
top: 35px;
right: -10px;
width: 120px;
padding-top: 8px; /* 增加感应连续性 */
z-index: 9999;
}
.dropdown-triangle {
position: absolute;
top: 2px;
right: 25px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
}
.dropdown-menu-body {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
padding: 6px 0;
}
.menu-item {
padding: 8px 16px;
font-size: 14px;
color: #606266;
text-align: center;
line-height: 1.5;
&:hover {
background-color: #f5f7fa;
color: #1890ff;
}
}
.danger-item {
&:hover {
color: #ff4d4f;
}
}
.p-img { width: 40px; height: 40px; border-radius: 4px; }
.p-name-txt { font-size: 13px; line-height: 1.4; color: #333; }
.activity-tags {
.op-divider { color: #e8e8e8; font-size: 12px; margin: 0 4px; }
}
.more-dropdown {
display: flex;
flex-direction: row;
align-items: center;
.arrow { font-size: 10px; color: #1890ff; margin-left: 2px; }
}
.pagination-row {
padding: 24px;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 16px;
.total { font-size: 13px; color: #606266; }
}
.page-ctrl {
display: flex;
flex-direction: row;
gap: 8px;
.page-num, .page-btn {
width: 32px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #606266;
&.active { background: #1890ff; color: #fff; border-color: #1890ff; }
&.disabled { color: #c0c4cc; background: #f5f7fa; }
}
}
</style>