|
@@ -1,18 +1,40 @@
|
|
|
<template>
|
|
<template>
|
|
|
<CustomNav title="药品信息管理" leftType="back" />
|
|
<CustomNav title="药品信息管理" leftType="back" />
|
|
|
<view class="page-container">
|
|
<view class="page-container">
|
|
|
- <view class="search-container">
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- v-model="searchKeyword"
|
|
|
|
|
- class="search-input"
|
|
|
|
|
- placeholder="搜索药品名称或拼音首字母"
|
|
|
|
|
- @input="handleSearch"
|
|
|
|
|
- />
|
|
|
|
|
- <button class="add-btn" @click="showAddModal">添加药品</button>
|
|
|
|
|
|
|
+ <view class="action-bar">
|
|
|
|
|
+ <view class="search-wrap">
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="search-input"
|
|
|
|
|
+ placeholder="搜索药品名称或拼音首字母"
|
|
|
|
|
+ v-model="searchKeyword"
|
|
|
|
|
+ @confirm="handleSearchImmediate"
|
|
|
|
|
+ @input="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <view class="search-btn" @click="handleSearchImmediate">
|
|
|
|
|
+ <uni-icons type="search" size="22" color="#fff" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="isLoading" class="skeleton-list">
|
|
|
|
|
+ <view v-for="i in 3" :key="i" class="skeleton-card">
|
|
|
|
|
+ <view class="skeleton-meta">
|
|
|
|
|
+ <view class="skeleton-line title" />
|
|
|
|
|
+ <view class="skeleton-line summary" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="skeleton-actions">
|
|
|
|
|
+ <view class="skeleton-btn" />
|
|
|
|
|
+ <view class="skeleton-btn small" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else-if="filteredMedicines.length === 0" class="empty-state">
|
|
|
|
|
+ <text class="empty-text">暂无药品</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <view class="medicine-list">
|
|
|
|
|
|
|
+ <view class="medicine-list" v-else>
|
|
|
<view
|
|
<view
|
|
|
class="medicine-item"
|
|
class="medicine-item"
|
|
|
v-for="medicine in filteredMedicines"
|
|
v-for="medicine in filteredMedicines"
|
|
@@ -84,6 +106,9 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <view class="fab" @click="showAddModal" role="button" aria-label="新建药品">
|
|
|
|
|
+ <view class="fab-inner">+</view>
|
|
|
|
|
+ </view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
@@ -110,6 +135,8 @@ const medicines = ref<Medicine[]>([])
|
|
|
|
|
|
|
|
// 搜索关键词
|
|
// 搜索关键词
|
|
|
const searchKeyword = ref('')
|
|
const searchKeyword = ref('')
|
|
|
|
|
+// 加载态
|
|
|
|
|
+const isLoading = ref(false)
|
|
|
|
|
|
|
|
// 模态框控制
|
|
// 模态框控制
|
|
|
const showModal = ref(false)
|
|
const showModal = ref(false)
|
|
@@ -142,11 +169,28 @@ const filteredMedicines = computed(() => {
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// 处理搜索
|
|
|
|
|
-const handleSearch = () => {
|
|
|
|
|
|
|
+// 处理搜索(使用防抖以减少请求频率)
|
|
|
|
|
+// 简单防抖实现
|
|
|
|
|
+function debounce<T extends (...args: any[]) => void> (fn: T, wait = 400) {
|
|
|
|
|
+ let timer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
+ return function(this: any, ...args: Parameters<T>) {
|
|
|
|
|
+ if (timer) clearTimeout(timer)
|
|
|
|
|
+ timer = setTimeout(() => fn.apply(this, args), wait)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 当用户按下确认时,立即搜索
|
|
|
|
|
+const handleSearchImmediate = () => {
|
|
|
|
|
+ pagination.value.pageNum = 1
|
|
|
loadMedicineList()
|
|
loadMedicineList()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 防抖版搜索(绑定到 @input)
|
|
|
|
|
+const handleSearch = debounce(() => {
|
|
|
|
|
+ pagination.value.pageNum = 1
|
|
|
|
|
+ loadMedicineList()
|
|
|
|
|
+}, 420)
|
|
|
|
|
+
|
|
|
// 显示添加模态框
|
|
// 显示添加模态框
|
|
|
const showAddModal = () => {
|
|
const showAddModal = () => {
|
|
|
isEditing.value = false
|
|
isEditing.value = false
|
|
@@ -285,6 +329,7 @@ const scanBarcode = () => {
|
|
|
|
|
|
|
|
// 加载药品列表
|
|
// 加载药品列表
|
|
|
const loadMedicineList = async () => {
|
|
const loadMedicineList = async () => {
|
|
|
|
|
+ isLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
const params = {
|
|
const params = {
|
|
|
pageNum: pagination.value.pageNum,
|
|
pageNum: pagination.value.pageNum,
|
|
@@ -308,6 +353,9 @@ const loadMedicineList = async () => {
|
|
|
icon: 'none'
|
|
icon: 'none'
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
+ finally {
|
|
|
|
|
+ isLoading.value = false
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 页面加载时
|
|
// 页面加载时
|
|
@@ -324,35 +372,89 @@ onMounted(() => {
|
|
|
padding-bottom: 40rpx;
|
|
padding-bottom: 40rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.search-container {
|
|
|
|
|
|
|
+.action-bar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- padding: 20rpx;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 18rpx;
|
|
|
|
|
+ padding: 28rpx 20rpx;
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
|
margin-bottom: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.search-wrap {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ flex: 1; /* 自动撑满剩余空间 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.search-input {
|
|
.search-input {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- padding: 16rpx 20rpx;
|
|
|
|
|
- border-radius: 10rpx;
|
|
|
|
|
- border: 1rpx solid #eee;
|
|
|
|
|
|
|
+ height: 64rpx;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ padding: 0 20rpx;
|
|
|
|
|
+ border: 1rpx solid #e6e6e6;
|
|
|
font-size: 28rpx;
|
|
font-size: 28rpx;
|
|
|
|
|
+ background: #fff;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.add-btn {
|
|
|
|
|
- padding: 16rpx 24rpx;
|
|
|
|
|
- background-color: #3742fa;
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- border-radius: 10rpx;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
|
|
+.search-btn {
|
|
|
|
|
+ width: 64rpx;
|
|
|
|
|
+ height: 64rpx;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ background-color: #4a90e2;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 顶部创建按钮已移除,保留悬浮按钮作为主要新建入口 */
|
|
|
|
|
+
|
|
|
|
|
+/* 悬浮按钮 */
|
|
|
|
|
+.fab {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ right: 28rpx;
|
|
|
|
|
+ bottom: 160rpx;
|
|
|
|
|
+ width: 110rpx;
|
|
|
|
|
+ height: 110rpx;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ background: linear-gradient(180deg, #4a90e2, #2d8cf0);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ z-index: 1200;
|
|
|
|
|
+ border: none;
|
|
|
}
|
|
}
|
|
|
|
|
+.fab:active { transform: translateY(2rpx); }
|
|
|
|
|
+.fab:focus { outline: none; }
|
|
|
|
|
+.fab-inner { color: #fff; font-size: 56rpx; line-height: 56rpx }
|
|
|
|
|
|
|
|
.medicine-list {
|
|
.medicine-list {
|
|
|
padding: 0 20rpx;
|
|
padding: 0 20rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.empty-state {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 100rpx 40rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.empty-text { font-size: 36rpx; margin-top: 20rpx; color: #999; }
|
|
|
|
|
+.create-cta { margin-top: 18rpx; background-color: #2d8cf0; color: #fff; padding: 16rpx 28rpx; border-radius: 12rpx; }
|
|
|
|
|
+
|
|
|
|
|
+.skeleton-list { padding: 18rpx 18rpx; }
|
|
|
|
|
+.skeleton-card { background: #fff; display: flex; gap: 18rpx; padding: 16rpx; border-radius: 12rpx; margin-bottom: 16rpx; align-items: center; justify-content: space-between; }
|
|
|
|
|
+.skeleton-meta { flex: 1; display: flex; flex-direction: column; gap: 10rpx; }
|
|
|
|
|
+.skeleton-line { background: linear-gradient(90deg, #eee, #f6f6f6); border-radius: 6rpx; }
|
|
|
|
|
+.skeleton-line.title { width: 40%; height: 28rpx; }
|
|
|
|
|
+.skeleton-line.summary { width: 60%; height: 18rpx; }
|
|
|
|
|
+.skeleton-actions { display:flex; gap: 12rpx; align-items: center; }
|
|
|
|
|
+.skeleton-btn { width: 100rpx; height: 40rpx; background: linear-gradient(90deg, #eee, #f6f6f6); border-radius: 20rpx; }
|
|
|
|
|
+.skeleton-btn.small { width: 60rpx; }
|
|
|
|
|
+
|
|
|
.medicine-item {
|
|
.medicine-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|