Browse Source

Merge branch 'master' of http://git.huaml.com/zhh/front-end

shish 11 months ago
parent
commit
0134b2ce5d

+ 51 - 8
hdApp/src/admin/goods/list.vue

@@ -155,6 +155,8 @@ export default {
       catId: "",
       flowerNum:0,
       searchText:'',
+      lastSearchText:'', // 上次的搜索内容
+      searchTimer: null, // 搜索防抖定时器
       riseSwitch: false, // 价格上涨开关
       riseData: {}, // 价格上涨配置
       lookStock:0, // 1:有库存 2:无库存
@@ -203,6 +205,14 @@ export default {
     }
   },
 
+  onUnload() {
+    // 清理搜索定时器
+    if (this.searchTimer) {
+      clearTimeout(this.searchTimer);
+      this.searchTimer = null;
+    }
+  },
+
   methods: {
     init(){
       this.getCategoryData()
@@ -320,14 +330,47 @@ export default {
       });
     },
     searchFn(){
-      if (this.$util.isEmpty(this.searchText)){
-        this.resetList()
-        this.getGoodsList()
-      }else{
-        this.resetList()
-        getCategoryToGoods({status:'',page:this.list.page,flower:1,searchText:this.searchText,flowerNum:this.flowerNum,lookPrice:this.lookPrice,lookStock:this.lookStock}).then(res => {
-          this.completes(res)
-        })
+      // 清除之前的定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer);
+        this.searchTimer = null;
+      }
+
+      // 1. 首次点击搜索框:searchText 为空,lastSearchText 也为空 → 跳过搜索
+      if (this.$util.isEmpty(this.searchText) && this.$util.isEmpty(this.lastSearchText)) {
+        return;
+      }
+
+      // 2. 用户输入后清空:searchText 为空,但 lastSearchText 有值 → 恢复分类筛选
+      if (this.$util.isEmpty(this.searchText) && !this.$util.isEmpty(this.lastSearchText)) {
+        this.lastSearchText = '';
+        this.resetList();
+        this.getGoodsList();
+        return;
+      }
+
+      // 4. 重复相同搜索:searchText 与 lastSearchText 相同 → 跳过搜索
+      if (this.searchText === this.lastSearchText) {
+        return;
+      }
+
+      // 3. 正常输入搜索:searchText 有值且与 lastSearchText 不同 → 执行搜索(1秒后触发)
+      if (!this.$util.isEmpty(this.searchText)) {
+        this.searchTimer = setTimeout(() => {
+          this.lastSearchText = this.searchText;
+          this.resetList();
+          getCategoryToGoods({
+            status:'',
+            page:this.list.page,
+            flower:1,
+            searchText:this.searchText,
+            flowerNum:this.flowerNum,
+            lookPrice:this.lookPrice,
+            lookStock:this.lookStock
+          }).then(res => {
+            this.completes(res);
+          });
+        }, 1000);
       }
     },
     getGoodsList() {

+ 12 - 1
mallApp/src/components/item/module/app-search.vue

@@ -44,6 +44,11 @@
 export default {
 	name: "app-search-module",
 	props: {
+		// v-model 支持
+		value: {
+			type: String,
+			default: ""
+		},
 		// 搜索组件的高度
 		height: {
 			type: Number,
@@ -96,9 +101,15 @@ export default {
 	},
 	data() {
 		return {
-			search: ""
+			search: this.value || ""
 		};
 	},
+	watch: {
+		// 监听外部 v-model 的变化
+		value(newVal) {
+			this.search = newVal || "";
+		}
+	},
 	methods: {
 		searchFn() {
 			this.$emit("search", this.search);

+ 256 - 34
mallApp/src/pages/home/category.vue

@@ -10,6 +10,23 @@
 			</view>
 		</view>
 
+		<!-- 搜索框区域 -->
+		<view class="search-wrap">
+			<view class="search-bar">
+				<app-search-module 
+					v-model="searchText" 
+					:placeholder="isSearching ? '正在搜索...' : '搜索商品名称'" 
+					@input="searchFn" 
+					:height="66"
+					:fontSize="28"
+				/>
+			</view>
+			<!-- 搜索状态提示 -->
+			<view v-if="isSearching" class="search-loading">
+				<text class="loading-text">正在搜索...</text>
+			</view>
+		</view>
+
 		<view class="buy-material-container">
 			<button class="buy-material-btn" @click="buyProduct()">
 				<view class="btn-content">
@@ -19,13 +36,40 @@
 		</view>
 
     <scroll-view scroll-y scroll-with-animation class="tab-view" :scroll-top="scrollTop">
-      <view v-for="(item,index) in tabbar" :key="index" class="tab-bar-item" :class="[currentTab==index ? 'active' : '']" :data-current="index" @tap.stop="swichNav($event ,item)" >
+      <view v-for="(item,index) in tabbar" :key="index" class="tab-bar-item" :class="[!isSearchMode && currentTab==index ? 'active' : '']" :data-current="index" @tap.stop="swichNav($event ,item)" >
         <text>{{item.categoryName}}</text>
       </view>
       <view style="height:150upx"></view>
     </scroll-view>
+    
+    <!-- 搜索结果展示区域 -->
+    <scroll-view v-if="isSearchMode" scroll-y class="right-box" @scrolltolower="reachBottom">
+      <view class="page-view" v-if="!$util.isEmpty(list.data)" >
+        <view class="goods-list" v-for="(items) in list.data" :key="items.id" @click="toBuy(items)" >
+          <view class="img-blo">
+            <image :src="items.smallCover" mode="aspectFill" ></image>
+          </view>
+          <view class="goods-det">
+            <view class="goods-det-top">
+              <view class="goods-name">{{ items.name }}</view>
+              <view class="goods-sales"></view>
+            </view>
+            <view class="goods-price">¥{{ items.price?parseFloat(items.price):0 }}</view>
+            <view class="buy-btn">购买</view>
+          </view>
+        </view>
+      </view>
+      <block v-else>
+        <app-wrapper-empty 
+          :title="getEmptyTitle()" 
+          :is-empty="$util.isEmpty(list.data)" 
+        />
+      </block>
+    </scroll-view>
+
+    <!-- 分类商品展示区域 -->
     <block v-for="(item,index) in tabbar" :key="index" >
-      <scroll-view scroll-y class="right-box" @scrolltolower="onReachBottoms" v-if="currentTab==index" >
+      <scroll-view scroll-y class="right-box" @scrolltolower="reachBottom" v-if="!isSearchMode && currentTab==index" >
         <view class="page-view" v-if="!$util.isEmpty(list.data)" >
           <view class="goods-list" v-for="(items) in list.data" :key="items.id" @click="toBuy(items)" >
             <view class="img-blo">
@@ -42,13 +86,15 @@
           </view>
         </view>
         <block v-else>
-          <app-wrapper-empty title="暂无商品" :is-empty="$util.isEmpty(list.data)" />
+          <app-wrapper-empty 
+            :title="getEmptyTitle()" 
+            :is-empty="$util.isEmpty(list.data)" 
+          />
         </block>
       </scroll-view>
     </block>
 
     <wangCg :loginStyle.sync="globalLoginStyle" @goToLogin="loginTo()"></wangCg>
-
   </view>
 </template>
 <script>
@@ -56,6 +102,7 @@ import { mapGetters } from 'vuex'
 import AppImg from '@/components/app-img'
 import wangCg from "@/components/wangCg"
 import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import AppSearchModule from '@/components/item/module/app-search'
 import { getClass, getList } from '@/api/category'
 import { list,share } from '@/mixins'
 import { getInfo } from "@/api/shop"
@@ -64,7 +111,8 @@ export default {
   components: {
     AppWrapperEmpty,
     AppImg,
-    wangCg
+    wangCg,
+    AppSearchModule
   },
   mixins: [list, share],
   data () {
@@ -80,11 +128,16 @@ export default {
       myClass: 'fadeIn',
 			shopName:'',
 			shopImg:'',
-      hdId:0
+      hdId:0,
+      searchText: '', // 搜索文本
+      searchTimer: null, // 搜索防抖定时器
+      isSearchMode: false, // 是否为搜索模式
+      isSearching: false, // 是否正在搜索
+      lastSearchText: '' // 上次搜索的文本,用于优化
     }
   },
 	computed: {
-    	...mapGetters({ loginInfo:"getLoginInfo" })
+    ...mapGetters({ loginInfo:"getLoginInfo" })
 	},
   onShareAppMessage(res) {
 		return {
@@ -100,6 +153,14 @@ export default {
 			this.globalLoginStyle = 0
 		}
   },
+  onUnload() {
+    // 清理搜索定时器
+    if (this.searchTimer) {
+      clearTimeout(this.searchTimer);
+      this.searchTimer = null;
+    }
+  },
+
 	watch: {
 		loginInfo: {
 			deep: true,
@@ -114,21 +175,21 @@ export default {
 		}
 	},
   methods: {
-	toBuy(item){
-	  let account = this.option.account?this.option.account:0
-	  this.$util.pageTo({url:"/pages/goods/detail?id="+item.id+'&hdId='+this.hdId+'&categoryId='+this.form.categoryId+'&account='+account})
-	},
-	buyProduct(){
-		let account = this.option.account?this.option.account:0
-		this.$util.pageTo({url:"/pages/item/item?account="+account+'&hdId='+this.hdId,type:2})
-	},
-	loginTo() {
-		let account = this.option.account?this.option.account:0
-		this.$util.pageTo({url:"/pages/login/index?needBack=1&account="+account})
-	},
-	init(){
-	  this.hdId = this.option.hdId ? this.option.hdId : 0
-	  this.classInit()
+    toBuy(item){
+      let account = this.option.account?this.option.account:0
+      this.$util.pageTo({url:"/pages/goods/detail?id="+item.id+'&hdId='+this.hdId+'&categoryId='+this.form.categoryId+'&account='+account})
+    },
+    buyProduct(){
+      let account = this.option.account?this.option.account:0
+      this.$util.pageTo({url:"/pages/item/item?account="+account+'&hdId='+this.hdId,type:2})
+    },
+    loginTo() {
+      let account = this.option.account?this.option.account:0
+      this.$util.pageTo({url:"/pages/login/index?needBack=1&account="+account})
+    },
+    init(){
+      this.hdId = this.option.hdId ? this.option.hdId : 0
+      this.classInit()
 			getInfo({hdId:this.hdId}).then(res=>{
 				if(res.code == 1){
 					if(res.data.info && res.data.info.merchantName){
@@ -148,19 +209,108 @@ export default {
 				}
 			})
     },
-    onReachBottoms () {
-      if (!this.list.finished) {
-        this.getGoodList().then(res => {
-          uni.stopPullDownRefresh()
-        });
+    // 搜索输入处理(防抖)
+    searchFn() {
+      console.log('searchFn----------', this.searchText, 'lastSearchText:', this.lastSearchText)
+      
+      // 清除之前的定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+        this.searchTimer = null
+      }
+      
+      // 如果搜索文本为空
+      if (this.$util.isEmpty(this.searchText)) {
+        // 如果之前也是空文本(首次点击或焦点清空),跳过搜索
+        if (this.$util.isEmpty(this.lastSearchText)) {
+          console.log('搜索文本为空且之前也为空,跳过搜索')
+          return
+        }
+        // 如果之前有搜索文本,现在清空了,则恢复分类筛选
+        this.clearSearch()
+        return
+      }
+      
+      // 如果搜索文本与上次相同,不重复搜索
+      if (this.searchText === this.lastSearchText) {
+        console.log('搜索文本未变化,跳过搜索')
+        return
+      }
+      
+      // 设置防抖定时器,1秒后执行搜索
+      this.searchTimer = setTimeout(() => {
+        this.performSearch()
+      }, 1000)
+    },
+    
+    // 执行搜索
+    performSearch() {
+      if (this.$util.isEmpty(this.searchText)) {
+        return
+      }
+      
+      console.log('执行搜索:', this.searchText)
+      
+      this.isSearchMode = true
+      this.isSearching = true
+      this.lastSearchText = this.searchText
+      this.resetList()
+      this.searchGoods()
+    },
+    
+    // 清空搜索,恢复分类筛选
+    clearSearch() {
+      console.log('清空搜索,恢复分类筛选')
+      this.isSearchMode = false
+      this.isSearching = false
+      this.lastSearchText = ''
+      
+      // 确保搜索框内容被清空
+      this.$nextTick(() => {
+        this.searchText = ''
+      })
+      
+      this.resetList()
+      this.getGoodList()
+    },
+    
+    // 搜索商品
+    searchGoods() {
+      return getList({ 
+        searchText: this.searchText, 
+        page: this.list.page, 
+        pageSize: 10 
+      }).then(res => {
+        this.isSearching = false
+        this.completes(res)
+        console.log('搜索成功,数据已加载')
+      }).catch(err => {
+        this.isSearching = false
+        console.error('搜索失败:', err)
+        this.$msg('搜索失败,请重试')
+      })
+    },
+    reachBottom () {
+      console.log('reachBottom----------', this.list.finished)
+      if (this.list.finished == false && !this.isSearching) {
+        // 根据是否为搜索模式决定调用哪个方法
+        if (this.isSearchMode) {
+          this.searchGoods().then(res => {
+            uni.stopPullDownRefresh()
+          });
+        } else {
+          this.getGoodList().then(res => {
+            uni.stopPullDownRefresh()
+          });
+        }
       } else {
         uni.stopPullDownRefresh()
       }
     },
     async classInit () {
       this.getMyClass() .then(res => {
-          this.getGoodList()
-        })
+        this.getGoodList()
+      })
     },
     // 分类
     getMyClass () {
@@ -173,18 +323,24 @@ export default {
     },
     // 列表
     getGoodList () {
-      return getList({ ...this.form, page: this.list.page,pageSize:100 }) .then(res => {
+      return getList({ ...this.form, page: this.list.page,pageSize:10 }) .then(res => {
+          // 处理商品列表数据,completes方法来自list混入
+          // 该方法会处理分页数据,更新list对象的data、page、finished等属性
+          // res包含接口返回的商品列表数据,completes方法会将数据添加到this.list.data中
           this.completes(res)
       })
     },
     // 点击标题切换当前页时改变样式
     swichNav (e, item) {
       let cur = e.currentTarget.dataset.current
-      if (this.currentTab == cur) {
+      if (this.currentTab == cur && !this.isSearchMode) {
         return false
       } else {
         this.currentTab = cur
         this.form.categoryId = item.id
+        
+        // 切换分类时清空搜索状态
+        this.clearSearchState()
         this.resetList()
         this.getGoodList()
       }
@@ -203,6 +359,42 @@ export default {
     },
     tabClick (index) {
       this.currentTabIndex = index
+    },
+    
+    // 清空搜索状态(不触发数据重新加载)
+    clearSearchState() {
+      // 清除定时器
+      if (this.searchTimer) {
+        clearTimeout(this.searchTimer)
+        this.searchTimer = null
+      }
+      
+      // 重置搜索相关状态
+      this.searchText = ''
+      this.isSearchMode = false
+      this.isSearching = false
+      this.lastSearchText = ''
+      
+      // 确保搜索框内容被清空(强制更新)
+      // this.$nextTick(() => {
+      //   this.searchText = ''
+      // })
+    },
+    
+    // 获取空状态提示文字
+    getEmptyTitle() {
+      if (this.isSearchMode && this.searchText) {
+        return `未找到"${this.searchText}"相关商品`
+      }
+      return '暂无商品'
+    }
+  },
+  
+  // 组件销毁时清理定时器
+  beforeDestroy() {
+    if (this.searchTimer) {
+      clearTimeout(this.searchTimer)
+      this.searchTimer = null
     }
   }
 }
@@ -281,7 +473,7 @@ page {
   position: fixed;
   left: 0;
   z-index: 10;
-  height: calc(100vh - 100upx);
+  height: calc(100vh - 160upx); /* 调整高度以适应搜索框 */
   background-color: #f0f2f6;
   .tab-bar-item {
     width: 200upx;
@@ -312,7 +504,7 @@ page {
 .right-box {
   width: 100%;
   overflow: auto;
-  height: calc(100vh - 220upx);
+  height: calc(100vh - 280upx); /* 调整高度以适应搜索框 */
   padding-left: 220upx;
   box-sizing: border-box;
   .page-view {
@@ -361,10 +553,40 @@ page {
   }
 }
 
+/* 搜索框样式 */
+.search-wrap {
+	padding: 20upx 30upx 10upx 30upx;
+	background: linear-gradient(to bottom, #3385FF, #fff);
+}
+
+.search-bar {
+	background-color: #fff;
+	border-radius: 30upx;
+	padding: 10upx;
+	box-shadow: 0upx 2px 8px 0upx rgba(0, 0, 0, 0.1);
+	position: relative;
+}
+
+.search-loading {
+	position: absolute;
+	right: 20upx;
+	top: 50%;
+	transform: translateY(-50%);
+	z-index: 100;
+	
+	.loading-text {
+		font-size: 24upx;
+		color: #999;
+		background-color: rgba(255, 255, 255, 0.9);
+		padding: 5upx 10upx;
+		border-radius: 10upx;
+	}
+}
+
 .buy-material-container {
 	position: absolute;
 	right: 20upx;
-	top: 148upx;
+	top: 190upx; /* 调整位置以适应搜索框 */
 	z-index: 9999;
 }
 

+ 48 - 60
mallApp/src/plugins/luch-request_0.0.7/request.js

@@ -9,7 +9,7 @@
 import qs from 'qs'
 import constant from '@/constant/index'
 // import common from '@/utils/common'
-import { parse, getCheckLogin } from '@/utils/util'
+import { parse } from '@/utils/util'
 import store from '@/store'
 
 const Base64 = require('js-base64').Base64
@@ -34,10 +34,14 @@ class Request {
 		complete() { }
 	}
 
-	static posUrl(url) { /* 判断url是否为绝对路径 */
+	static isAbsoluteUrl(url) { /* 判断url是否为绝对路径 */
 		return /(http|https):\/\/([\w.]+\/?)\S*/.test(url)
 	}
 
+	static posUrl(url) { /* 兼容旧方法名 */
+		return Request.isAbsoluteUrl(url)
+	}
+
 	interceptor = {
 		request: (f) => {
 			if (f && typeof f === 'function') {
@@ -70,9 +74,7 @@ class Request {
 		}
 		// const token = uni.getStorageSync('token')
 		// config.data = JSON.stringify(config.data);
-		config.headers = {
-			'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
-		}
+		// 统一使用 config.header,避免混用 headers
 		// if (token) { // 判断是否存在token,如果存在的话,则每个http header都加上token
 		// 	// axios.defaults.headers.common['token'] = token;
 		// 	config.headers.token = token
@@ -80,7 +82,7 @@ class Request {
 		// config.headers.HkdsRequestFrom = window.location.href
 		// config.header.userRegion = Base64.encode(uni.getStorageSync('userRegion')) || null
 		// config.header['x-client-hash'] = uni.getStorageSync('client_hash') || null
-		const userRegion = Base64.encode(uni.getStorageSync('userRegion')) || null
+		// const userRegion = Base64.encode(uni.getStorageSync('userRegion')) || null
 		// if (userRegion) {
 		// 	config.header.userRegion = userRegion
 		// }
@@ -119,10 +121,43 @@ class Request {
 		return response
 	}
 
+	/**
+	 * 处理鉴权失效后的通用逻辑
+	 * @param {string} currentRoute 当前页面路由
+	 */
+	handleAuthExpired(currentRoute) {
+		uni.removeStorageSync('token')
+		store.commit('setLoginInfo', {})
+		uni.showToast({
+			title: '加载中...',
+			icon: 'none',
+			mask: true
+		})
+		const account = uni.getStorageSync('account')
+		if (account) {
+			setTimeout(() => {
+				uni.reLaunch({
+					url: '/' + currentRoute + '?account=' + account
+				})
+			}, 1000)
+		} else {
+			setTimeout(() => {
+				uni.reLaunch({
+					url: `/pages/home/recent`
+				})
+			}, 1000)
+		}
+	}
+
 	setConfig(f) {
 		this.config = f(this.config)
 	}
 
+	/**
+	 * 发送 HTTP 请求
+	 * @param {Object} options uni.request 的参数子集
+	 * @param {Object} config 业务控制参数 { loading?: boolean, hideError?: boolean }
+	 */
 	request(options = {}, config) {
 		options.baseUrl = options.baseUrl || this.config.baseUrl
 		options.dataType = options.dataType || this.config.dataType
@@ -146,6 +181,7 @@ class Request {
 				// 全局忽略错误,在单个请求里面自行处理
 				if (config.hideError) {
 					resolve(response.data)
+					return
 				}
 				if (statusCode === 200) { // 成功
 			
@@ -153,8 +189,6 @@ class Request {
 					if (response.data.code === 1) {
 						resolve(response.data)
 					} else {
-						
-						let noLogin = ['pages/pay/index','pages/callback/success']
 						let currentPages = getCurrentPages()[0].route
 						// 返回错误信息
 						if (response.data.code == 0) {
@@ -168,55 +202,9 @@ class Request {
 							}
 							reject(response.data)
 						} else if (response.data.code && response.data.code == -1) {
-							uni.removeStorageSync('token')
-							store.commit('setLoginInfo', {})
-
-							uni.showToast({
-								title: '加载中...',
-								icon: 'none',
-								mask: true
-							})
-							
-							let account = uni.getStorageSync('account')
-							if(account){
-								setTimeout(() => {
-									uni.reLaunch({
-										url: '/'+currentPages+'?account='+account
-									})
-								}, 1000)
-							}else{
-								setTimeout(() => {
-									uni.reLaunch({
-										url: `/pages/home/recent`
-									})
-								}, 1000)
-							}
-
+							this.handleAuthExpired(currentPages)
 						} else if (response.data.code == 2) {
-							uni.removeStorageSync('token')
-							store.commit('setLoginInfo', {})
-							
-							uni.showToast({
-								title: '加载中...',
-								icon: 'none',
-								mask: true
-							})
-							
-							let account = uni.getStorageSync('account')
-							if(account){
-								setTimeout(() => {
-									uni.reLaunch({
-										url: '/'+currentPages+'?account='+account
-									})
-								}, 1000)
-							}else{
-								setTimeout(() => {
-									uni.reLaunch({
-										url: `/pages/home/recent`
-									})
-								}, 1000)
-							}
-
+							this.handleAuthExpired(currentPages)
 						} else {
 							console.log('response.data', response.data)
 							if (!config.hideError) {
@@ -250,8 +238,8 @@ class Request {
 	}
 
 	get(url, data = {}, options = {}, config = {}) {
-		let account = uni.getStorageSync('account') || 0;
-		let hdId = uni.getStorageSync('hdId') || 0;
+		const account = uni.getStorageSync('account') || 0;
+		const hdId = uni.getStorageSync('hdId') || 0;
 		options.url = url + parse({...data,'account':account,'hdId':hdId})
 		options.data = {}
 		options.method = 'GET'
@@ -259,8 +247,8 @@ class Request {
 	}
 
 	post(url, data = {}, options = {}, config = {}) {
-		let account = uni.getStorageSync('account') || 0;
-		let hdId = uni.getStorageSync('hdId') || 0;
+		const account = uni.getStorageSync('account') || 0;
+		const hdId = uni.getStorageSync('hdId') || 0;
 		options.url = url+'?account='+account+'&hdId='+hdId
 		options.data = data
 		options.method = 'POST'