Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿

This commit is contained in:
2026-02-28 05:40:21 +08:00
commit 14d29d51c0
2182 changed files with 482509 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
<template>
<view class="productSort">
<view class='nav acea-row row-middle'>
<scroll-view class="scroll-view_x" scroll-x scroll-with-animation :scroll-left="scrollLeft" style="width:auto;overflow:hidden;">
<view class='item' v-for="(item,index) in navLists" :key='index' :class='active==index?"on":""' @click='tabSelect(index,item.id)'
:id="'id'+index">
<view>{{item.name}}</view>
<view class='line' v-if="active==index"></view>
</view>
</scroll-view>
</view>
<view class='list acea-row row-between-wrapper'>
<view class='item' v-for="(item,index) in productList" :key="index" @click="godDetail(item)">
<view class='pictrue'>
<image :src='item.image'></image>
<view :style="{ backgroundImage: `url(${item.activityStyle})` }" class="border-picture"></view>
</view>
<view class='text'>
<view class='name line1'>{{item.storeName}}</view>
<view class='money'><text class='num'>{{item.price}}</text></view>
<view class='vip acea-row row-between-wrapper'>
<view>已售{{item.sales + item.ficti}}</view>
</view>
</view>
</view>
<view class='loadingicon acea-row row-center-wrapper' v-if="productList.length">
<text class='loading iconfont icon-jiazai' :hidden='loading==false'></text>{{loadTitle}}
</view>
</view>
<view class='noCommodity' v-if="productList.length== 0 && page > 1">
<view class='pictrue'>
<image :src="urlDomain+'crmebimage/perset/staticImg/noShopper.png'"></image>
</view>
<recommend ref="recommendIndex"></recommend>
</view>
</view>
</template>
<script>
import {
getCategoryList,
getProductslist
} from '@/api/store.js';
import {
goShopDetail
} from '@/libs/order.js'
import recommend from '@/components/recommend';
import {
mapGetters
} from "vuex";
import animationType from '@/utils/animationType.js'
export default {
computed: mapGetters(['uid']),
components: {
recommend,
},
data() {
return {
urlDomain: this.$Cache.get("imgHost"),
navLists: [],
productList: [],
scrollLeft: 0,
active: 0,
loading: false,
loadend: false,
loadTitle: '加载更多',
page: 1,
limit: 10,
cid: 515,
hostProduct: []
}
},
created(){
this.getAllCategory();
this.getProductList();
},
methods: {
// 去详情页
godDetail(item) {
goShopDetail(item, this.uid).then(res => {
uni.navigateTo({
animationType: animationType.type, animationDuration: animationType.duration,
url: `/pages/goods/goods_details/index?id=${item.id}`
})
})
},
tabSelect(index, id) {
this.active = index;
const query = uni.createSelectorQuery().in(this);
query.select('#id' + index).boundingClientRect(data => {
this.scrollLeft = (index - 1) * data.width;
}).exec();
this.cid = id;
this.loadend = false;
this.page = 1;
this.$set(this, 'productList', [])
this.getProductList();
},
getAllCategory: function() {
let that = this;
getCategoryList().then(res => {
that.navLists = res.data;
let pid= uni.getStorageSync('categoryId');
console.log('pid123',pid);
if(pid){
let indexNow = that.navLists.findIndex(item=>item.id==pid)
this.tabSelect(indexNow,pid)
uni.removeStorageSync('categoryId');
}
})
},
getProductList: function() {
let that = this;
if (that.loadend) return;
if (that.loading) return;
that.loading = true;
that.loadTitle = '';
getProductslist({
page: that.page,
limit: that.limit,
cid: that.cid
}).then(res => {
let list = res.data.list,
loadend = list.length < that.limit;
that.productList = that.$util.SplitArray(list, that.productList);
that.$set(that, 'productList', that.productList);
that.loading = false;
that.loadend = loadend;
that.loadTitle = loadend ? "哼😕~我也是有底线的~" : "加载更多";
that.page = that.page + 1;
}).catch(err => {
that.loading = false,
that.loadTitle = '加载更多'
});
},
},
onReachBottom() {
this.$refs.recommendIndex.get_host_product();
}
}
</script>
<style scoped lang="scss">
.productSort {
.nav {
padding: 0 30rpx;
width: 100%;
white-space: nowrap;
box-sizing: border-box;
height: 86rpx;
background-color: #fff;
position: fixed;
top: 0;
left: 0;
z-index: 9;
.item {
display: inline-block;
font-size: 30rpx;
color: #282828;
padding-right: 46rpx;
&.on {
color: #4B56AA;
font-weight: bold;
}
.line {
width: 40rpx;
height: 4rpx;
background-color: #4B56AA;
margin: 10rpx auto 0 auto;
}
}
}
.list {
margin-top: 86rpx;
padding: 0 20rpx;
.item {
width: 345rpx;
margin-top: 20rpx;
background-color: #fff;
border-radius: 20rpx;
.pictrue {
position: relative;
width: 100%;
height: 345rpx;
image {
width: 100%;
height: 100%;
border-radius: 20rpx 20rpx 0 0;
}
}
.text {
padding: 20rpx 17rpx 26rpx 17rpx;
font-size: 30rpx;
color: #222;
.money {
font-size: 26rpx;
font-weight: bold;
margin-top: 8rpx;
@include price_color(theme);
.num {
font-size: 34rpx;
}
}
}
}
}
}
.scroll-Y{
height: 100vh;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view :data-theme="theme">
<view class='productSort'>
<skeleton :show="showSkeleton" :isNodes="isNodes" ref="skeleton" loading="chiaroscuro" selector="skeleton"
bgcolor="#FFF"></skeleton>
<view class="skeleton" :style="{visibility: showSkeleton ? 'hidden' : 'visible'}">
<view class='header acea-row row-center-wrapper'>
<view class='acea-row row-between-wrapper input'>
<text class='iconfont icon-sousuo'></text>
<input type='text' placeholder='点击搜索商品信息' @confirm="searchSubmitValue" confirm-type='search' name="search"
placeholder-class='placeholder' maxlength="20"></input>
</view>
</view>
<view class='aside' :style="{bottom: tabbarH + 'px',height: height + 'rpx'}">
<scroll-view scroll-y="true" scroll-with-animation='true' style="height: 100%;">
<view class='item acea-row row-center-wrapper' :class='index==navActive?"on":""' v-for="(item,index) in productList"
:key="index" @click='tap(index,"b"+index)'><text class="skeleton-rect">{{item.name}}</text></view>
</scroll-view>
</view>
<view class='conter'>
<scroll-view scroll-y="true" :scroll-into-view="toView" :style='"height:"+height+"rpx;margin-top: 96rpx;"' @scroll="scroll"
scroll-with-animation='true'>
<block v-for="(item,index) in productList" :key="index">
<view class='listw' :id="'b'+index">
<view class='title acea-row row-center-wrapper'>
<view class='line'></view>
<view class='name'>{{item.name}}</view>
<view class='line'></view>
</view>
<view class='list acea-row'>
<block v-for="(itemn,indexn) in item.child" :key="indexn">
<navigator hover-class='none' :url='"/pages/goods/goods_list/index?cid="+itemn.id+"&title="+itemn.name' class='item acea-row row-column row-middle'>
<view class='picture skeleton-rect' :style="{'background-color':itemn.extra?'none':'#f7f7f7'}">
<image :src='itemn.extra'></image>
</view>
<view class='name line1'>{{itemn.name}}</view>
</navigator>
</block>
</view>
</view>
</block>
<view :style='"height:"+(height-300)+"rpx;"' v-if="number<15"></view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
import {getCategoryList} from '@/api/store.js';
import ClipboardJS from "@/plugin/clipboard/clipboard.js";
import animationType from '@/utils/animationType.js'
export default {
data() {
return {
showSkeleton: true, //骨架屏显示隐藏
isNodes: 0, //控制什么时候开始抓取元素节点,只要数值改变就重新抓取
navlist: [],
productList: [{name:'占位占位',child:[{extra:''},{extra:''}]},{name:'占位占位',child:[{extra:''},{extra:''}]},{name:'占位占位',child:[{extra:''},{extra:''}]},{name:'占位占位'}],
navActive: 0,
number: "",
height: 0,
hightArr: [],
toView: "",
tabbarH: 0,
theme:'theme1'
}
},
created() {
let _self = this;
uni.getStorage({
key: 'theme',
success: function (res) {
_self.theme = res.data;
}
});
setTimeout(() => {
this.isNodes++;
}, 500);
this.getAllCategory();
},
onShow() {
this.getAllCategory();
},
methods: {
infoScroll: function() {
let that = this;
let len = that.productList.length;
let child = that.productList[len - 1]&&that.productList[len - 1].child?that.productList[len - 1].child:[];
this.number = child?child.length:0;
//设置商品列表高度
uni.getSystemInfo({
success: function(res) {
that.height = (res.windowHeight) * (750 / res.windowWidth) - 98;
},
});
let height = 0;
let hightArr = [];
for (let i = 0; i < len; i++) {
//获取元素所在位置
let query = uni.createSelectorQuery().in(this);
let idView = "#b" + i;
query.select(idView).boundingClientRect();
query.exec(function(res) {
let top = res[0].top;
hightArr.push(top);
that.hightArr = hightArr
});
};
},
tap: function(index, id) {
this.toView = id;
this.navActive = index;
},
getAllCategory: function() {
let that = this;
getCategoryList().then(res => {
that.productList = res.data;
let pid= uni.getStorageSync('categoryId');
if(pid){
let indexNow = that.productList.findIndex(item=>item.id==pid)
this.tap(indexNow,'b'+indexNow)
}
setTimeout(function(){
that.infoScroll();
},500)
setTimeout(() => {
this.showSkeleton = false
}, 1000)
})
},
scroll: function(e) {
let scrollTop = e.detail.scrollTop + 10;
let scrollArr = this.hightArr;
for (let i = 0; i < scrollArr.length; i++) {
if (scrollTop >= 0 && scrollTop < scrollArr[1] - scrollArr[0]) {
this.navActive = 0
} else if (scrollTop >= scrollArr[i] - scrollArr[0] && scrollTop < scrollArr[i + 1] - scrollArr[0]) {
this.navActive = i
} else if (scrollTop >= scrollArr[scrollArr.length - 1] - scrollArr[0]) {
this.navActive = scrollArr.length - 1
}
}
},
searchSubmitValue: function(e) {
if (this.$util.trim(e.detail.value).length > 0)
uni.navigateTo({
animationType: animationType.type,
animationDuration: animationType.duration,
url: '/pages/goods/goods_list/index?searchValue=' + e.detail.value
})
else
return this.$util.Tips({
title: '请填写要搜索的产品信息'
});
},
}
}
</script>
<style scoped lang="scss">
.productSort .header {
width: 100%;
height: 96rpx;
background-color: #fff;
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 9;
border-bottom: 1rpx solid #f5f5f5;
}
.productSort .header .input {
width: 700rpx;
height: 60rpx;
background-color: #f5f5f5;
border-radius: 50rpx;
box-sizing: border-box;
padding: 0 25rpx;
}
.productSort .header .input .iconfont {
font-size: 26rpx;
color: #555;
}
.productSort .header .input .placeholder {
color: #999;
}
.productSort .header .input input {
font-size: 26rpx;
height: 100%;
width: 597rpx;
}
.productSort .aside {
position: fixed;
width: 180rpx;
left: 0;
top:0;
background-color: #f7f7f7;
overflow-y: scroll;
overflow-x: hidden;
height: auto;
margin-top: 96rpx;
}
.productSort .aside .item {
height: 100rpx;
width: 100%;
font-size: 26rpx;
color: #424242;
position: relative;
}
.productSort .aside .item.on {
background-color: #fff;
width: 100%;
text-align: center;
@include main_color(theme);
font-weight: bold;
}
.productSort .aside .item.on ::before{
content: '';
width: 4rpx;
height: 100rpx;
position: absolute;
left: 0;
top: 0;
@include main_bg_color(theme);
}
.productSort .conter {
margin: 96rpx 0 0 180rpx;
padding: 0 14rpx;
background-color: #fff;
}
.productSort .conter .listw {
padding-top: 20rpx;
}
.productSort .conter .listw .title {
height: 90rpx;
}
.productSort .conter .listw .title .line {
width: 100rpx;
height: 2rpx;
background-color: #f0f0f0;
}
.productSort .conter .listw .title .name {
font-size: 28rpx;
color: #333;
margin: 0 30rpx;
font-weight: bold;
}
.productSort .conter .list {
flex-wrap: wrap;
}
.productSort .conter .list .item {
width: 177rpx;
margin-top: 26rpx;
}
.productSort .conter .list .item .picture {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
}
.productSort .conter .list .item .picture image {
width: 100%;
height: 100%;
border-radius: 50%;
div{
background-color: #f7f7f7;
}
}
.productSort .conter .list .item .name {
font-size: 24rpx;
color: #333;
height: 56rpx;
line-height: 56rpx;
width: 120rpx;
text-align: center;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
<template>
<view class="page" :data-theme="theme" :style="{height:winHeight + 'px'}">
<cate v-show="currentPage == 'one'"></cate>
<contracted v-show="currentPage == 'two'" ref="classTwo"></contracted>
<optimization v-show="currentPage == 'three'" :showSlide="showSlide" ref="classThree"></optimization>
<fresh v-show="currentPage == 'four'" :showSlide="showSlide" ref="classFour"></fresh>
<pageFooter v-if="footerShow"></pageFooter>
</view>
</template>
<script>
import pageFooter from '@/components/pageFooter/index.vue'
import cate from './components/default_cate';
import optimization from './components/optimization';
import contracted from './components/contracted';
import fresh from './components/fresh';
import {getShare} from '@/api/public.js';
import {mapGetters} from 'vuex';
const app = getApp();
export default {
data() {
return {
footerShow:true,
currentPage:'one',
theme:app.globalData.theme,
showSlide:true,
winHeight:'',
configApi: {}, //分享类容配置
}
},
computed: mapGetters(['isLogin', 'uid']),
onLoad(){
let that = this;
let config = that.$Cache.getItem('categoryConfig');
that.showSlide = config.isShowCategory == 'true'? true : false;
switch (config.categoryConfig) {
case '1':
that.$set(that,'currentPage','one');
break;
case '2':
that.$set(that,'currentPage','two');
break;
case '3':
that.$set(that,'currentPage','three');
uni.hideTabBar()
this.footerShow=false
break;
case '4':
that.$set(that,'currentPage','four');
uni.hideTabBar()
this.footerShow=false
break;
}
uni.getSystemInfo({
success: function (res) {
that.winHeight = res.windowHeight;
}
});
// #ifdef H5
that.shareApi();
// #endif
},
onShow(){
switch (this.currentPage){
case 'one':
break;
case 'two':
break;
case 'three':
uni.hideTabBar()
this.footerShow=false
setTimeout(()=>{
if(this.isLogin){
//登录的情况下获取模板3,4的购物车商品数量和列表
this.$refs.classThree.getCartNum();
this.$refs.classThree.getCartLists(1);
}
},500)
break;
case 'four':
uni.hideTabBar()
this.footerShow=false
setTimeout(()=>{
if(this.isLogin){
this.$refs.classFour.getCartNum();
this.$refs.classFour.getCartLists(1);
}
},500)
break;
}
},
components:{
cate,optimization,contracted,fresh,pageFooter
},
methods:{
shareApi: function() {
getShare().then(res => {
this.$set(this, 'configApi', res.data);
// #ifdef H5
this.setOpenShare(res.data);
// #endif
})
},
// 微信分享;
setOpenShare: function(data) {
let that = this;
if (that.$wechat.isWeixin()) {
let configAppMessage = {
desc: data.synopsis,
title: data.title,
link: location.href,
imgUrl: data.img
};
that.$wechat.wechatEvevt(["updateAppMessageShareData", "updateTimelineShareData"],
configAppMessage);
}
},
},
onReachBottom(){
if(this.currentPage=='two'){
this.$refs.classTwo.getProductList();
}
if(this.currentPage=='three'){
this.$refs.classThree.productslist();
}
if(this.currentPage=='four'){
this.$refs.classFour.productslist();
}
}
}
</script>
<style lang="scss">
.page{
background: #fff;
height: 100% !important;
}
</style>