Files
xk-doctor-wx/subPackages/sub_business_shared/warehouse/index.vue
2026-06-18 10:20:24 +08:00

384 lines
9.2 KiB
Vue
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>
<business-page-layout :title="pageTitle" :show-back="true">
<!-- 搜索栏优化胶囊式设计增加视觉呼吸感 -->
<view class="search-wrapper">
<view class="search-bar">
<input class="search-input" v-model="keyword" placeholder="搜索药品名称" placeholder-class="search-placeholder" @confirm="reload" />
<view class="search-btn" hover-class="btn-hover" @click="reload">搜索</view>
</view>
</view>
<view class="list-container">
<view
v-for="item in list"
:key="item._wxKey"
class="item-card"
hover-class="card-hover"
@click="goDetail(item)"
>
<!-- 左侧缩略图 -->
<image class="thumb" :src="rowDisplay(item).imageUrl" mode="aspectFill" />
<!-- 右侧内容区 -->
<view class="content">
<!-- 顶部标题与状态标签对齐 -->
<view class="header-row">
<text class="title">{{ rowDisplay(item).drugName }}</text>
<view class="status-tag" :class="rowDisplay(item).isOnShelf ? 'tag-on' : 'tag-off'">
{{ rowDisplay(item).statusText }}
</view>
</view>
<!-- 中部详细信息 -->
<view class="info-group">
<text class="info-text">供货 {{ rowDisplay(item).buyPrice }} · 售价 {{ rowDisplay(item).price }}</text>
<text class="info-text">库存 {{ rowDisplay(item).stock }}</text>
</view>
<!-- 底部操作按钮横向排列增加点击区域与反馈 -->
<view class="action-row" @click.stop>
<view class="action-btn outline" hover-class="btn-hover-opacity" @click.stop="openPriceEdit(item)">
改价
</view>
<view class="action-btn" :class="rowDisplay(item).isOnShelf ? 'btn-danger' : 'btn-primary'" hover-class="btn-hover-opacity" @click.stop="toggleStatus(item)">
{{ rowDisplay(item).isOnShelf ? '下架' : '上架' }}
</view>
</view>
</view>
</view>
</view>
<!-- 空状态优化 -->
<view v-if="!loading && !list.length" class="empty-state">
<view class="empty-icon"></view>
<text class="empty-text">暂无药品数据</text>
</view>
<price-edit-modal
:visible="priceModalVisible"
:item="priceEditItem"
@close="closePriceEdit"
@confirm="onPriceConfirm"
/>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import PriceEditModal from './components/PriceEditModal.vue'
import { withWxKey } from '@/utils/wxListKey.js'
import { mapWarehouseRow } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default {
mixins: [businessMixin],
components: { BusinessPageLayout, PriceEditModal },
data() {
return {
pageTitle: '仓库管理',
warehouseType: '',
keyword: '',
list: [],
page: 1,
loading: false,
priceModalVisible: false,
priceEditItem: null,
priceSubmitting: false,
}
},
onLoad(q) {
if (q.label) {
this.pageTitle = decodeURIComponent(q.label)
}
if (q.type != null && q.type !== '') {
this.warehouseType = q.type
}
this.reload()
},
onReachBottom() {
this.load(this.page + 1, true)
},
methods: {
rowDisplay(item) {
return mapWarehouseRow(item, this.bizCap.warehouseType || 'store')
},
reload() {
this.page = 1
this.load(1, false)
},
load(page, append) {
this.loading = true
const params = { page, pageSize: 20 }
if (this.keyword) params.name = this.keyword
if (this.warehouseType !== '' && this.warehouseType != null) {
params.type = this.warehouseType
}
this.bizApi.getWarehouseList(params).then(w => {
const items = withWxKey((w.data && w.data.items) || [], 'id', 'wh')
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
goDetail(item) {
uni.navigateTo({ url: `${this.bizRoot}/warehouse/detail?id=${item.id}` })
},
openPriceEdit(item) {
this.priceEditItem = item
this.priceModalVisible = true
},
closePriceEdit() {
this.priceModalVisible = false
this.priceEditItem = null
},
onPriceConfirm(payload) {
if (!this.priceEditItem || this.priceSubmitting) return
this.priceSubmitting = true
const data = typeof payload === 'object' && payload !== null
? { id: this.priceEditItem.id, ...payload }
: { id: this.priceEditItem.id, price: payload }
this.bizApi.updateWarehousePrice(data).then(w => {
if (w.ok) {
uni.showToast({ title: '修改成功' })
this.closePriceEdit()
this.reload()
}
}).finally(() => { this.priceSubmitting = false })
},
toggleStatus(item) {
const action = item.status === 2 ? '下架' : '上架'
uni.showModal({
title: '提示',
content: '确定' + action + '「' + ((item.drug && item.drug.drug_name) || '') + '」?',
success: (res) => {
if (!res.confirm) return
this.bizApi.updateWarehouseStatus(item.id).then(w => {
if (w.ok) {
uni.showToast({ title: '操作成功' })
item.status = item.status === 2 ? 1 : 2
}
})
},
})
},
},
}
</script>
<style lang="scss" scoped>
/* 核心主题色变量提取,方便后续统一管理 */
$theme-color: #6acdbb;
$theme-color-light: rgba(106, 205, 187, 0.12);
$text-primary: #2c3e50;
$text-secondary: #8a97a3;
$text-light: #b0b8c1;
$bg-color: #f7f9fa;
$border-color: #ebedf0;
$danger-color: #ff6b6b;
$danger-color-light: rgba(255, 107, 107, 0.1);
.search-wrapper {
padding: 16rpx 24rpx;
background-color: transparent;
position: sticky;
top: 0;
z-index: 10;
}
.search-bar {
display: flex;
align-items: center;
background: #ffffff;
padding: 12rpx 16rpx 12rpx 32rpx;
border-radius: 40rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.search-input {
flex: 1;
font-size: 28rpx;
color: $text-primary;
height: 60rpx;
}
.search-placeholder {
color: $text-light;
}
.search-btn {
background: $theme-color;
color: #ffffff;
font-size: 26rpx;
font-weight: 500;
padding: 12rpx 36rpx;
border-radius: 30rpx;
margin-left: 16rpx;
transition: all 0.2s;
}
.list-container {
padding: 0 24rpx 24rpx;
}
.item-card {
display: flex;
background: #ffffff;
padding: 24rpx;
border-radius: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
transition: transform 0.2s, box-shadow 0.2s;
}
.card-hover {
transform: scale(0.99);
background-color: #fafafa;
}
.thumb {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
flex-shrink: 0;
background: $bg-color;
border: 1rpx solid $border-color;
}
.content {
flex: 1;
margin-left: 24rpx;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8rpx;
}
.title {
font-size: 30rpx;
color: $text-primary;
font-weight: 600;
line-height: 1.4;
flex: 1;
margin-right: 16rpx;
/* 单行截断 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
font-size: 20rpx;
font-weight: 500;
padding: 4rpx 14rpx;
border-radius: 8rpx;
flex-shrink: 0;
line-height: 1.5;
}
.tag-on {
color: $theme-color;
background: $theme-color-light;
}
.tag-off {
color: $text-secondary;
background: $bg-color;
}
.info-group {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-bottom: 16rpx;
}
.info-text {
font-size: 24rpx;
color: $text-secondary;
}
.action-row {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16rpx;
margin-top: auto;
}
.action-btn {
font-size: 24rpx;
padding: 10rpx 28rpx;
border-radius: 12rpx;
font-weight: 500;
line-height: 1.5;
transition: opacity 0.2s;
}
.action-btn.outline {
color: $text-primary;
background: #ffffff;
border: 1rpx solid $border-color;
}
.action-btn.btn-primary {
color: #ffffff;
background: $theme-color;
border: 1rpx solid $theme-color;
}
.action-btn.btn-danger {
color: $danger-color;
background: $danger-color-light;
border: 1rpx solid transparent;
}
.btn-hover {
opacity: 0.8;
}
.btn-hover-opacity {
opacity: 0.6;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
background-color: $bg-color;
border-radius: 50%;
margin-bottom: 30rpx;
position: relative;
}
/* 用伪元素做一个简单的空状态图形占位有条件可替换为真实的Image/SVG */
.empty-icon::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
border: 8rpx solid $border-color;
border-radius: 16rpx;
}
.empty-text {
color: $text-light;
font-size: 28rpx;
}
</style>