Files
medical-mall/pages/mall/admin/product/product-management/index.uvue
2026-02-06 12:06:33 +08:00

618 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:'浣i噾绠$悊寮€鍙戜腑', icon:'none'})">浣i噾绠$悊</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'
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 铔嬪3妞?杩涘彛澶村眰鐗涚毊姗欒壊鍗曚汉娌欏彂妞匛gg 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 {
/* padding removed */
}
.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>