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

623 lines
16 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">
<view class="mock-switch" :class="{ on: item.status === 1 }">
<text class="switch-txt">{{ item.status === 1 ? '上架' : '下架' }}</text>
<view class="switch-dot"></view>
</view>
</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, onMounted } from 'vue'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
import { fetchAdminProductPage, updateAdminProductStatus, type AdminProduct } from '@/services/admin/productService.uts'
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const activeStatus = ref<number | null>(1) // 1:出售中
const activeDropdownId = ref<string | null>(null)
const productList = ref<Array<AdminProduct>>([])
const searchName = ref('')
const statusTabs = ref([
{ key: 1, label: '出售中的商品', count: 0 },
{ key: 2, label: '仓库中的商品', count: 0 },
{ key: 3, label: '草稿箱', count: 0 },
{ key: 4, label: '回收站', count: 0 },
])
onMounted(() => {
loadData()
})
async function loadData() {
const res = await fetchAdminProductPage(page.value, pageSize.value, {
name: searchName.value,
status: activeStatus.value ?? undefined
})
productList.value = res.items
total.value = res.total
}
function handleSearch() {
page.value = 1
loadData()
}
function handleReset() {
searchName.value = ''
page.value = 1
loadData()
}
async function toggleStatus(item: AdminProduct) {
const newStatus = item.status === 1 ? 2 : 1
const ok = await updateAdminProductStatus(item.id, newStatus)
if (ok) {
uni.showToast({ title: '操作成功', icon: 'success' })
loadData()
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
async function moveToRecycle(item: AdminProduct) {
const isDelete = activeStatus.value !== 4
const targetStatus = isDelete ? 4 : 2 // 移到回收站或恢复到下架
const action = isDelete ? '移到回收站' : '恢复'
uni.showModal({
title: '提示',
content: `确认要将该商品${action}吗?`,
success: async (res) => {
if (res.confirm) {
const ok = await updateAdminProductStatus(item.id, targetStatus)
if (ok) {
uni.showToast({ title: '操作成功', icon: 'success' })
loadData()
}
}
}
})
}
function changeStatus(key: number) {
activeStatus.value = key
page.value = 1
loadData()
}
function goEdit(id: string | null) {
openRoute('product_edit')
}
function goReviews(id: string) {
openRoute('product_productReply')
}
function goMemberPrice(id: string) {
openRoute('product_member_price')
}
</script>
<style scoped lang="scss">
.product-list-page {
padding: 20px;
background-color: #f5f7f9;
min-height: 100vh;
}
.search-card {
background: #fff;
padding: 24px;
border-radius: 4px;
margin-bottom: 16px;
}
.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 {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
.tag {
padding: 2px 4px;
font-size: 11px;
color: #fff;
border-radius: 2px;
&.kj { background: #1890ff; }
&.pt { background: #52c41a; }
&.ms { background: #f5222d; }
}
}
.mock-switch {
width: 50px;
height: 20px;
background: #dbdbdb;
border-radius: 10px;
position: relative;
display: flex;
align-items: center;
padding: 0 4px;
&.on {
background: #1890ff;
.switch-dot { left: 32px; }
.switch-txt { left: 6px; }
}
&:not(.on) {
.switch-txt { right: 6px; }
.switch-dot { left: 2px; }
}
.switch-txt {
position: absolute;
font-size: 10px;
color: #fff;
}
.switch-dot {
position: absolute;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transition: left 0.2s;
}
}
.op-link {
font-size: 13px;
color: #1890ff;
cursor: pointer;
&.danger { color: #ff4d4f; }
}
.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>