淘宝授权推送
This commit is contained in:
parent
acd448b6dd
commit
d1c0419d76
17
App.vue
17
App.vue
|
|
@ -3,6 +3,23 @@
|
|||
|
||||
export default {
|
||||
globalData: {
|
||||
/**
|
||||
* urlParams - 当前页面 URL 参数集合
|
||||
*
|
||||
* 来源:
|
||||
* 1. H5 环境下从 window.location(search + hash)解析
|
||||
* 2. 小程序/App 环境下从页面栈 currentPage.options 获取
|
||||
* 3. 分享/扫码进入时从 App.onLaunch/onShow 的 options.query 合并
|
||||
*
|
||||
* 目前业务中实际使用的参数:
|
||||
* - token {string} 用户登录凭证
|
||||
* - code {string} 淘宝后返回授权码
|
||||
* - state {string} 状态值,格式通常为 "uidxxx",用于提取用户 ID(淘宝授权返回)
|
||||
* - exuid {string} 推广人/邀请人用户 ID(三方进入用户标识)
|
||||
* - page_uri {string} 授权成功后需要跳转的目标页面路径(必须授权过的用户该参数才有效)
|
||||
*
|
||||
* 注:UrlQuery 支持获取任意 URL 参数,以上仅为业务代码中显式消费的字段
|
||||
*/
|
||||
urlParams: {}
|
||||
},
|
||||
onLaunch: function(options) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
activeTab: {
|
||||
|
|
@ -25,17 +27,14 @@
|
|||
},
|
||||
methods: {
|
||||
getBottomBar() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data.bottom_bar) {
|
||||
http.get('https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx').then(res => {
|
||||
if (res.data && res.data.bottom_bar) {
|
||||
// 过滤并排序:剔除“发现”,仅保留首页、榜单、分类
|
||||
const allowedTitles = ['首页', '榜单', '分类'];
|
||||
this.bottomBarList = res.data.data.bottom_bar
|
||||
this.bottomBarList = res.data.bottom_bar
|
||||
.filter(item => allowedTitles.includes(item.title))
|
||||
.sort((a, b) => b.sort - a.sort);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
handleTabClick(tab, index) {
|
||||
|
|
|
|||
4
main.js
4
main.js
|
|
@ -1,9 +1,10 @@
|
|||
import App from './App'
|
||||
|
||||
import { estimateCoupon } from './utils/index.js'
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.$estimateCoupon = estimateCoupon;
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
|
|
@ -17,6 +18,7 @@ import store from './store'
|
|||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(store)
|
||||
app.config.globalProperties.$estimateCoupon = estimateCoupon;
|
||||
return {
|
||||
app
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
const API_BASE = 'https://v2.api.haodanku.com';
|
||||
const CUSTOM_PARAMS = {
|
||||
apikey: '5417B681C5EA'
|
||||
|
|
@ -156,12 +157,10 @@
|
|||
...this.getListDataParams()
|
||||
};
|
||||
|
||||
uni.request({
|
||||
url: `${API_BASE}/get_index_activity_items`,
|
||||
data: params,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const block = res.data.data ? (res.data.data.block || []) : (res.data.block || []);
|
||||
http.get(`${API_BASE}/get_index_activity_items`, params)
|
||||
.then(res => {
|
||||
const body = res.body || {};
|
||||
const block = body.data ? (body.data.block || []) : (body.block || []);
|
||||
this.category.list = block.map((item, index) => ({
|
||||
label: item.name,
|
||||
value: String(index)
|
||||
|
|
@ -170,21 +169,16 @@
|
|||
const list = this.category.goodsLists[0] || [];
|
||||
this.listData.list = list;
|
||||
this.listData.finished = true;
|
||||
} else {
|
||||
})
|
||||
.catch(() => {
|
||||
this.listData.list = [];
|
||||
this.listData.finished = true;
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.listData.list = [];
|
||||
this.listData.finished = true;
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.listData.loading = false;
|
||||
this.loading = false;
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -247,16 +241,11 @@
|
|||
|
||||
if (!isWechatEnv) {
|
||||
// 非微信环境:弹窗确认后跳转
|
||||
uni.request({
|
||||
url: `${API_BASE}/ratesurl`,
|
||||
method: 'POST',
|
||||
data: param,
|
||||
header: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const data = res.data.data || res.data;
|
||||
http.post(`${API_BASE}/ratesurl`, param, {
|
||||
header: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}).then(res => {
|
||||
const body = res.body || {};
|
||||
const data = body.data || body;
|
||||
const jumpUrl = data.coupon_click_url || data.item_url;
|
||||
if (jumpUrl) {
|
||||
uni.showModal({
|
||||
|
|
@ -286,32 +275,16 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
});
|
||||
} else {
|
||||
// 微信环境:获取淘口令并复制
|
||||
uni.request({
|
||||
url: `${API_BASE}/ratesurl`,
|
||||
method: 'POST',
|
||||
data: param,
|
||||
header: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const data = res.data.data || res.data;
|
||||
http.post(`${API_BASE}/ratesurl`, param, {
|
||||
header: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}).then(res => {
|
||||
const body = res.body || {};
|
||||
const data = body.data || body;
|
||||
const taoword = data.taoword;
|
||||
if (taoword) {
|
||||
const taocode = '0' + taoword + '/';
|
||||
|
|
@ -326,24 +299,10 @@
|
|||
}
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
}
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getEnvironment } from '@/utils/env.js';
|
||||
import { getEnvironment } from '@/utils/env.js';
|
||||
import http from '@/request/request.js';
|
||||
import pagesConfig from '@/pages.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
client_id: '30004725',
|
||||
|
|
@ -69,11 +71,12 @@
|
|||
onLoad() {
|
||||
const app = getApp();
|
||||
const params = app.globalData.urlParams || {};
|
||||
|
||||
this.Token = params.token || '';
|
||||
this.code = params.code || '';
|
||||
this.queryState = params.state || '';
|
||||
this.$store.dispatch('setCurrentUid', (params.state || '').replace(/^uid/, '') || '')
|
||||
this.exuid = params.exuid || '';
|
||||
if(this.queryState) this.$store.dispatch('setCurrentUid', (params.state || '').replace(/^uid/, '') || '');
|
||||
|
||||
|
||||
this.handleAuth();
|
||||
},
|
||||
|
|
@ -85,13 +88,14 @@
|
|||
},
|
||||
methods: {
|
||||
getBaseUrl() {
|
||||
// return 'https://point.agrimedia.cn';
|
||||
return window.location.origin;
|
||||
// // #ifdef H5
|
||||
// return window.location.origin;
|
||||
// // #endif
|
||||
// // #ifndef H5
|
||||
// // 非 H5 平台(小程序/App)需配置为已备案的 H5 域名
|
||||
// return 'https://tpoint.agrimedia.cn';
|
||||
return 'https://tpoint.agrimedia.cn';
|
||||
// // #endif
|
||||
},
|
||||
|
||||
|
|
@ -99,26 +103,53 @@
|
|||
const baseUrl = this.getBaseUrl();
|
||||
if (this.Token) {
|
||||
// 场景1:授权检测,通过 token 获取用户信息
|
||||
uni.request({
|
||||
url: baseUrl + '/api/user',
|
||||
header: {
|
||||
'authori-zation': `Bearer ${this.Token}`
|
||||
http.get(`${baseUrl}/api/user`, null, {
|
||||
header: { 'authori-zation': `Bearer ${this.Token}` }
|
||||
}).then(this.handleUserInfo).catch(this.handleUserError);
|
||||
} else if (this.code && this.queryState) {
|
||||
// 场景2:淘宝授权回调(带 code + state)
|
||||
http.get(`${baseUrl}/api/taobao/execute`, {
|
||||
code: this.code,
|
||||
state: this.queryState
|
||||
}).then(res => {
|
||||
const data = res.data || {};
|
||||
this.setAuthData(data.relation_id, data.tbk_pid);
|
||||
this.showSuccess('授权已完成,跳转中...');
|
||||
}).catch(err => {
|
||||
console.error('授权失败:', err);
|
||||
const result = err.raw?.data;
|
||||
const msg = result?.msg || result?.message || err.message || '授权处理失败,请重新授权';
|
||||
this.showFail(msg);
|
||||
});
|
||||
} else if (this.exuid) {
|
||||
// 场景3:三方uid授权
|
||||
this.$store.dispatch('setIsThirdParty', true);
|
||||
|
||||
http.get(`${baseUrl}/api/taobao/third/relation_id`, {
|
||||
uid: this.exuid
|
||||
}).then(this.handleUserInfo).catch(this.handleUserError);
|
||||
}else{
|
||||
this.showFail('授权链接异常,请重新授权!');
|
||||
}
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const data = res.data.data || {};
|
||||
const relationId = data.relation_id;
|
||||
|
||||
handleUserInfo(res) {
|
||||
const data = res.data || {};
|
||||
const relationId = Number(data.relation_id);
|
||||
const uid = data.uid;
|
||||
const pid = data.tbk_pid;
|
||||
if (relationId) {
|
||||
// 已授权
|
||||
this.$store.dispatch('setRelationId', relationId);
|
||||
// 其实无论是否授权过,都请求都会返回有值的 pid,但是只有授权过才能正常使用
|
||||
this.setAuthData(relationId, pid);
|
||||
|
||||
// 此处为处理跳转指定页面逻辑
|
||||
const app = getApp();
|
||||
const params = app.globalData.urlParams || {};
|
||||
if(params.page_uri) {
|
||||
if (params?.page_uri && this.isValidPagePath(params.page_uri)) {
|
||||
uni.navigateTo({
|
||||
url: params.page_uri
|
||||
});
|
||||
}else{
|
||||
} else {
|
||||
this.goHome();
|
||||
}
|
||||
} else if (uid) {
|
||||
|
|
@ -128,49 +159,27 @@
|
|||
} else {
|
||||
this.showFail('获取用户信息失败!');
|
||||
}
|
||||
} else {
|
||||
this.showFail(res.data?.msg || res.data?.message || '获取用户信息失败');
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
|
||||
handleUserError(err) {
|
||||
console.error('获取用户信息失败:', err);
|
||||
this.showFail('获取用户信息失败,请检查网络');
|
||||
}
|
||||
});
|
||||
} else if (this.code && this.queryState) {
|
||||
// 场景2:淘宝授权回调(带 code + state)
|
||||
uni.request({
|
||||
url: baseUrl + '/api/taobao/execute',
|
||||
data: {
|
||||
code: this.code,
|
||||
state: this.queryState
|
||||
const msg = err.raw?.data?.msg || err.raw?.data?.message || err.message || '获取用户信息失败,请检查网络';
|
||||
this.showFail(msg);
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const result = res.data;
|
||||
// 兼容 status / code 两种业务状态码
|
||||
const status = Number(result.status != null ? result.status : result.code);
|
||||
if (status === 200 || status === 1) {
|
||||
const data = result.data || {};
|
||||
this.$store.dispatch('setRelationId', data.relation_id);
|
||||
this.showSuccess('授权已完成,跳转中...');
|
||||
} else {
|
||||
this.showFail(result.msg || result.message || '授权处理失败,请重新授权');
|
||||
}
|
||||
} else {
|
||||
this.showFail('授权处理失败,请重新授权');
|
||||
}
|
||||
|
||||
setAuthData(relationId, pid) {
|
||||
this.$store.dispatch('setRelationId', relationId);
|
||||
this.$store.dispatch('setPid', pid || 'mm_284380119_1881450385_111415850448');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('授权失败:', err);
|
||||
this.showFail('授权处理失败,请重新授权');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.showFail('授权链接异常,请重新授权!');
|
||||
// this.$store.dispatch('setRelationId', '123456789');
|
||||
// this.showSuccess('授权已完成,跳转中...');
|
||||
|
||||
isValidPagePath(pageUri) {
|
||||
if (!pageUri) return false;
|
||||
let path = pageUri.startsWith('/') ? pageUri.slice(1) : pageUri;
|
||||
const queryIndex = path.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
path = path.slice(0, queryIndex);
|
||||
}
|
||||
return pagesConfig.pages.some(page => page.path === path);
|
||||
},
|
||||
|
||||
goHome() {
|
||||
|
|
@ -204,7 +213,8 @@
|
|||
this.status = 'success';
|
||||
this.title = '授权成功';
|
||||
this.desc = message;
|
||||
this._t = setTimeout(() => this.goHome(), 500);
|
||||
// this._t = setTimeout(() => this.goHome(), 500);
|
||||
this.goHome()
|
||||
},
|
||||
|
||||
CopyStatus(message) {
|
||||
|
|
@ -266,11 +276,11 @@
|
|||
return baseUrl + '/affiliate-activity/pages/auth/auth';
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f2f5 0%, #f7f8fa 100%);
|
||||
display: flex;
|
||||
|
|
@ -278,9 +288,9 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-box {
|
||||
.auth-box {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 100rpx 56rpx 80rpx;
|
||||
|
|
@ -288,21 +298,21 @@
|
|||
max-width: 640rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
.auth-icon {
|
||||
margin: 0 auto 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.spin-animation {
|
||||
.spin-animation {
|
||||
animation: spin 1.2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
|
@ -310,25 +320,25 @@
|
|||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
.auth-title {
|
||||
font-size: 44rpx;
|
||||
color: #1a1a1a;
|
||||
margin: 24rpx 0 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-desc {
|
||||
.auth-desc {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 56rpx;
|
||||
line-height: 1.8;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
.auth-btn {
|
||||
margin-top: 32rpx;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
|
|
@ -343,27 +353,27 @@
|
|||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-btn:active {
|
||||
.auth-btn:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-btn::after {
|
||||
.auth-btn::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-btn--primary {
|
||||
.auth-btn--primary {
|
||||
background: linear-gradient(135deg, #ff715a, #ff416c);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 65, 108, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
.btn-text {
|
||||
color: inherit;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@
|
|||
<text class="coupon-txt">{{ goods.couponValue }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-coupon" v-if="!$store.state.isThirdParty">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(goods.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="goods-bottom-info">
|
||||
|
|
@ -103,6 +109,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -154,12 +161,10 @@
|
|||
});
|
||||
},
|
||||
getCategoryTabs() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/superCategory?is_get_second=1&cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
http.get('https://api.cmspro.haodanku.com/index/superCategory?is_get_second=1&cid=YsWZ21tx').then(res => {
|
||||
if (res.data) {
|
||||
// 映射为与首页一致的 navList 结构
|
||||
const categories = res.data.data.map(item => ({
|
||||
const categories = res.data.map(item => ({
|
||||
name: item.name,
|
||||
cat_id: item.cat_id,
|
||||
second: item.second || []
|
||||
|
|
@ -171,7 +176,6 @@
|
|||
this.subCategories = currentCat.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getProducts(refresh = false) {
|
||||
|
|
@ -191,16 +195,15 @@
|
|||
sortParam = this.priceOrder === 'asc' ? 8 : 9;
|
||||
}
|
||||
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/find/allItemList?category_id=${this.currentCatId}&page=${this.page}&sort=${sortParam}&page_size=20&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && res.data.data.item_info) {
|
||||
const list = res.data.data.item_info.map(item => ({
|
||||
http.get(`https://api.cmspro.haodanku.com/find/allItemList?category_id=${this.currentCatId}&page=${this.page}&sort=${sortParam}&page_size=20&cid=YsWZ21tx`).then(res => {
|
||||
if (res.data && res.data.item_info) {
|
||||
const list = res.data.item_info.map(item => ({
|
||||
id: item.id,
|
||||
image: item.itempic,
|
||||
title: item.itemshorttitle && item.itemshorttitle.length > 17 ? item.itemshorttitle.substring(0, 17) + '...' : item.itemshorttitle,
|
||||
finalPrice: item.itemendprice,
|
||||
couponValue: item.couponmoney || 0,
|
||||
tkmoney: item.tkmoney || 0,
|
||||
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale,
|
||||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
|
||||
shopName: item.shopname,
|
||||
|
|
@ -221,10 +224,8 @@
|
|||
} else {
|
||||
this.finished = true;
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
switchCategory(item) {
|
||||
|
|
@ -625,6 +626,23 @@
|
|||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.price-coupon {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-val {
|
||||
font-size: 28rpx;
|
||||
color: #ff8a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.goods-bottom-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -33,26 +33,33 @@
|
|||
|
||||
<!-- 高级筛选标签栏 -->
|
||||
<view class="sub-filter-row">
|
||||
<view class="sub-filter-item" :class="{ 'active': filterHasCoupon }" @click="toggleFilter('coupon')">有券</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsFlagship }" @click="toggleFilter('flagship')">旗舰店</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsTmall }" @click="toggleFilter('tmall')">天猫</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsBrand }" @click="toggleFilter('brand')">品牌</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterHasCoupon }" @click="toggleFilter('coupon')">有券
|
||||
</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsFlagship }" @click="toggleFilter('flagship')">
|
||||
旗舰店</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsTmall }" @click="toggleFilter('tmall')">天猫
|
||||
</view>
|
||||
<view class="sub-filter-item" :class="{ 'active': filterIsBrand }" @click="toggleFilter('brand')">品牌
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 (横向单列风格) -->
|
||||
<view class="goods-list">
|
||||
<view class="goods-item" v-for="(goods, idx) in goodsList" :key="idx" @click="goToDetail('id', goods.id)">
|
||||
<view class="goods-item" v-for="(goods, idx) in goodsList" :key="idx"
|
||||
@click="goToDetail('id', goods.id)">
|
||||
<view class="g-img-left">
|
||||
<image class="goods-img" :src="goods.image" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="goods-info">
|
||||
<view class="goods-title-row">
|
||||
<image src="https://img-haodanku-com.cdn.fudaiapp.com/FlyOSTvjC3LjrkUoJ0NPxx1qnGz4" class="platform-icon"></image>
|
||||
<image src="https://img-haodanku-com.cdn.fudaiapp.com/FlyOSTvjC3LjrkUoJ0NPxx1qnGz4"
|
||||
class="platform-icon"></image>
|
||||
<text class="title-text">{{ goods.title }}</text>
|
||||
</view>
|
||||
<!-- 标签组 -->
|
||||
<view class="labels-row" v-if="goods.labels && goods.labels.length > 0">
|
||||
<text class="label-tag" v-for="(label, lIdx) in goods.labels.slice(0, 2)" :key="lIdx">{{ label }}</text>
|
||||
<text class="label-tag" v-for="(label, lIdx) in goods.labels.slice(0, 2)"
|
||||
:key="lIdx">{{ label }}</text>
|
||||
</view>
|
||||
|
||||
<view class="goods-price-section">
|
||||
|
|
@ -65,6 +72,12 @@
|
|||
<text class="coupon-txt">{{ goods.couponValue }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-coupon" v-if="!$store.state.isThirdParty">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(goods.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="goods-bottom-info">
|
||||
|
|
@ -85,17 +98,20 @@
|
|||
|
||||
<!-- 推荐商品列表 -->
|
||||
<view class="goods-list recommend-list" v-if="recommendList.length > 0">
|
||||
<view class="goods-item" v-for="(goods, idx) in recommendList" :key="idx" @click="goToDetail('keywordid', goods.id)">
|
||||
<view class="goods-item" v-for="(goods, idx) in recommendList" :key="idx"
|
||||
@click="goToDetail('keywordid', goods.id)">
|
||||
<view class="g-img-left">
|
||||
<image class="goods-img" :src="goods.image" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="goods-info">
|
||||
<view class="goods-title-row">
|
||||
<image src="https://img-haodanku-com.cdn.fudaiapp.com/FlyOSTvjC3LjrkUoJ0NPxx1qnGz4" class="platform-icon"></image>
|
||||
<image src="https://img-haodanku-com.cdn.fudaiapp.com/FlyOSTvjC3LjrkUoJ0NPxx1qnGz4"
|
||||
class="platform-icon"></image>
|
||||
<text class="title-text">{{ goods.title }}</text>
|
||||
</view>
|
||||
<view class="labels-row" v-if="goods.labels && goods.labels.length > 0">
|
||||
<text class="label-tag" v-for="(label, lIdx) in goods.labels.slice(0, 2)" :key="lIdx">{{ label }}</text>
|
||||
<text class="label-tag" v-for="(label, lIdx) in goods.labels.slice(0, 2)"
|
||||
:key="lIdx">{{ label }}</text>
|
||||
</view>
|
||||
<view class="goods-price-section">
|
||||
<view class="price-main">
|
||||
|
|
@ -107,6 +123,12 @@
|
|||
<text class="coupon-txt">{{ goods.couponValue }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-coupon" v-if="!$store.state.isThirdParty">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(goods.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="goods-bottom-info">
|
||||
<text class="sales">已售{{ goods.sales }}件</text>
|
||||
|
|
@ -129,6 +151,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -195,17 +218,20 @@
|
|||
filterParams += `&shoptype=${shopTypes.join(',')}`;
|
||||
}
|
||||
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/find/allItemList?category_id=${this.mainCatId}&son_category=${encodeURIComponent(this.secondCategory)}&page=${this.page}&sort=${sortParam}&page_size=20&cid=YsWZ21tx${filterParams}`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && res.data.data.item_info) {
|
||||
const list = res.data.data.item_info.map(item => ({
|
||||
http.get(
|
||||
`https://api.cmspro.haodanku.com/find/allItemList?category_id=${this.mainCatId}&son_category=${encodeURIComponent(this.secondCategory)}&page=${this.page}&sort=${sortParam}&page_size=20&cid=YsWZ21tx${filterParams}`
|
||||
).then(res => {
|
||||
if (res.data && res.data.item_info) {
|
||||
const list = res.data.item_info.map(item => ({
|
||||
id: item.id,
|
||||
image: item.itempic,
|
||||
title: item.itemshorttitle && item.itemshorttitle.length > 18 ? item.itemshorttitle.substring(0, 18) + '...' : item.itemshorttitle,
|
||||
title: item.itemshorttitle && item.itemshorttitle.length > 18 ? item
|
||||
.itemshorttitle.substring(0, 18) + '...' : item.itemshorttitle,
|
||||
finalPrice: item.itemendprice,
|
||||
couponValue: item.couponmoney || 0,
|
||||
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale,
|
||||
tkmoney: item.tkmoney || 0,
|
||||
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' :
|
||||
item.itemsale,
|
||||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
|
||||
shopName: item.shopname,
|
||||
labels: item.label || []
|
||||
|
|
@ -229,10 +255,8 @@
|
|||
this.finished = true;
|
||||
if (refresh) this.getRecommendList();
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
getRecommendList() {
|
||||
|
|
@ -244,30 +268,33 @@
|
|||
if (this.filterIsFlagship) recommendParams += '&is_tmall=1';
|
||||
if (this.filterIsTmall) recommendParams += '&min_id=1';
|
||||
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/superSearch/getList?sort=0&page_size=20&category_id=${this.mainCatId}&son_category=${encodeURIComponent(this.secondCategory)}&cid=YsWZ21tx${recommendParams}`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data) {
|
||||
const list = res.data.data.map(item => {
|
||||
console.log(item)
|
||||
http.get(
|
||||
`https://api.cmspro.haodanku.com/superSearch/getList?sort=0&page_size=20&category_id=${this.mainCatId}&son_category=${encodeURIComponent(this.secondCategory)}&cid=YsWZ21tx${recommendParams}`
|
||||
).then(res => {
|
||||
console.log('res', res)
|
||||
if (res.data) {
|
||||
const list = res.data.map(item => {
|
||||
console.log('推荐商品:', item.id || item.itemid)
|
||||
return {
|
||||
id: item.id || item.itemid,
|
||||
image: item.itempic,
|
||||
title: item.itemshorttitle && item.itemshorttitle.length > 18 ? item.itemshorttitle.substring(0, 18) + '...' : item.itemshorttitle,
|
||||
title: item.itemshorttitle && item.itemshorttitle.length > 18 ? item
|
||||
.itemshorttitle.substring(0, 18) + '...' : item.itemshorttitle,
|
||||
finalPrice: item.itemendprice,
|
||||
couponValue: item.couponmoney || 0,
|
||||
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale,
|
||||
tkmoney: item.tkmoney || 0,
|
||||
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' :
|
||||
item.itemsale,
|
||||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
|
||||
shopName: item.shopname,
|
||||
labels: item.label || (item.couponmoney > 0 ? [`${item.couponmoney}元券`] : [])
|
||||
labels: item.label || (item.couponmoney > 0 ? [`${item.couponmoney}元券`] :
|
||||
[])
|
||||
}
|
||||
});
|
||||
this.recommendList = list;
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
}).finally(() => {
|
||||
this.loadingRecommend = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
changeSort(type) {
|
||||
|
|
@ -290,7 +317,7 @@
|
|||
loadMore() {
|
||||
this.getProducts();
|
||||
},
|
||||
goToDetail(key,id) {
|
||||
goToDetail(key, id) {
|
||||
// console.log(`/pages/detail/detail?${key}=${id}`)
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/detail?${key}=${id}`
|
||||
|
|
@ -548,7 +575,7 @@
|
|||
.coupon-icon {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
background: rgba(255,255,255,0.2);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0 6rpx;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
@ -561,6 +588,23 @@
|
|||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.price-coupon {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-val {
|
||||
font-size: 28rpx;
|
||||
color: #ff8a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.goods-bottom-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
|
||||
<script>
|
||||
import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -75,29 +76,23 @@
|
|||
},
|
||||
methods: {
|
||||
getSuperCategory() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/superCategory?cid=YsWZ21tx',
|
||||
http.get('https://api.cmspro.haodanku.com/index/superCategory?cid=YsWZ21tx', undefined, {
|
||||
header: {
|
||||
'accept': 'application/json, text/plain, */*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
this.categoryData = res.data.data;
|
||||
}
|
||||
}).then(res => {
|
||||
this.categoryData = res.data;
|
||||
// 获取数据后尝试匹配底部导航高亮状态
|
||||
this.matchNavActive();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
matchNavActive() {
|
||||
// 获取底部导航数据,查找“分类”对应的索引
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data.bottom_bar) {
|
||||
http.get('https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx').then(res => {
|
||||
if (res.data && res.data.bottom_bar) {
|
||||
const allowedTitles = ['首页', '榜单', '分类'];
|
||||
const list = res.data.data.bottom_bar
|
||||
const list = res.data.bottom_bar
|
||||
.filter(item => allowedTitles.includes(item.title))
|
||||
.sort((a, b) => b.sort - a.sort);
|
||||
const idx = list.findIndex(item => item.title === '分类');
|
||||
|
|
@ -105,7 +100,6 @@
|
|||
this.navActiveIndex = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
switchMainCategory(index) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
<!-- 活动横幅 -->
|
||||
<view class="activity-banner" v-if="product.activity && product.activity.app_img">
|
||||
<image :src="product.activity.app_img" mode="widthFix" class="banner-img"></image>
|
||||
<view class="banner-time" v-if="product.activityTime">TIME: {{ product.activityTime }}</view>
|
||||
<!-- <view class="banner-time" v-if="product.activityTime">TIME: {{ product.activityTime }}</view> -->
|
||||
</view>
|
||||
|
||||
<!-- 热销榜单提示 -->
|
||||
|
|
@ -68,6 +68,9 @@
|
|||
<view class="tags-row">
|
||||
<text class="tag-capsule" v-for="(tag, index) in product.labels" :key="index">{{ tag }}</text>
|
||||
</view>
|
||||
<view class="estimate-tip" v-if="!$store.state.isThirdParty">
|
||||
<text>消费券:{{ $estimateCoupon(product.commission) }}为预估值,实际以系统发放为准</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券模块 -->
|
||||
|
|
@ -180,6 +183,12 @@
|
|||
<text>{{ item.couponmoney }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-left coupon-row" v-if="!$store.state.isThirdParty">
|
||||
<view class="d-coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(item.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-left flex-left-t">
|
||||
<view class="sales-volume">已售{{ formatSales(item.itemsale) }}件</view>
|
||||
</view>
|
||||
|
|
@ -439,7 +448,10 @@
|
|||
url: `https://api.cmspro.haodanku.com/detail/getRecommendItems?itemid=${itemId}&son_category=${sonCategory}&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
this.similarProducts = res.data.data.slice(0, 10);
|
||||
this.similarProducts = res.data.data.slice(0, 10).map(item => ({
|
||||
...item,
|
||||
tkmoney: item.tkmoney || 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -903,6 +915,22 @@
|
|||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.coupon-row {
|
||||
align-items: baseline;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-row .coupon-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-row .coupon-val {
|
||||
font-size: 26rpx;
|
||||
color: #ff8a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sales-volume {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
|
|
@ -1097,6 +1125,15 @@
|
|||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.estimate-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
background-color: #fff8f0;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
/* 优惠券 */
|
||||
.coupon-section {
|
||||
padding: 0 30rpx;
|
||||
|
|
|
|||
|
|
@ -240,6 +240,12 @@
|
|||
</view>
|
||||
<view class="g-coupon-tag">券 {{ goods.couponValue }}元</view>
|
||||
</view>
|
||||
<view class="price-coupon" v-if="!$store.state.isThirdParty">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(goods.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="goods-sales">已售 {{ goods.sales }} 件</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -255,6 +261,7 @@
|
|||
|
||||
<script>
|
||||
import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -354,11 +361,8 @@
|
|||
});
|
||||
},
|
||||
getIndexData() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/index?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data) {
|
||||
const d = res.data.data;
|
||||
http.get('https://api.cmspro.haodanku.com/index/index?cid=YsWZ21tx').then(res => {
|
||||
const d = res.data;
|
||||
if (Array.isArray(d.navs)) {
|
||||
this.menus = d.navs;
|
||||
}
|
||||
|
|
@ -370,8 +374,8 @@
|
|||
if (Array.isArray(d.tile_long) && d.tile_long.length > 0) {
|
||||
this.adList = d.tile_long.map(t => t.img).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取首页数据失败:', err.message);
|
||||
});
|
||||
},
|
||||
goToDetail(id) {
|
||||
|
|
@ -383,34 +387,27 @@
|
|||
},
|
||||
|
||||
getCategoryList() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/category?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data.category) {
|
||||
this.navList = [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }, ...res.data.data.category];
|
||||
}
|
||||
http.get('https://api.cmspro.haodanku.com/index/category?cid=YsWZ21tx').then(res => {
|
||||
if (res.data.category) {
|
||||
this.navList = [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }, ...res.data.category];
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取分类失败:', err.message);
|
||||
});
|
||||
},
|
||||
getNoticeList() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/msg/getMsgs?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
this.noticeList = res.data.data.msgs;
|
||||
if (res.data.data.num) {
|
||||
this.qiangNum = res.data.data.num;
|
||||
}
|
||||
}
|
||||
http.get('https://api.cmspro.haodanku.com/msg/getMsgs?cid=YsWZ21tx').then(res => {
|
||||
this.noticeList = res.data.msgs;
|
||||
if (res.data.num) {
|
||||
this.qiangNum = res.data.num;
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取通知失败:', err.message);
|
||||
});
|
||||
},
|
||||
getGoodsList() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/recommend/getRecommend?page_size=200&page=1&type=2&cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
const list = res.data.data.recommends.map(item => {
|
||||
http.get('https://api.cmspro.haodanku.com/recommend/getRecommend?page_size=200&page=1&type=2&cid=YsWZ21tx').then(res => {
|
||||
const list = res.data.recommends.map(item => {
|
||||
// 格式化销量
|
||||
let salesStr = item.itemsale;
|
||||
if (item.itemsale >= 10000) {
|
||||
|
|
@ -426,19 +423,17 @@
|
|||
sales: salesStr,
|
||||
brandTag: item.brand_name,
|
||||
lowestTag: item.label && item.label.length > 0 ? item.label[0] : '',
|
||||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝'
|
||||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
|
||||
tkmoney: item.tkmoney
|
||||
};
|
||||
});
|
||||
this.goodsList = list;
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取商品列表失败:', err.message);
|
||||
});
|
||||
},
|
||||
getWorthBuyLists() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/deserveLists?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
http.get('https://api.cmspro.haodanku.com/index/deserveLists?cid=YsWZ21tx').then(res => {
|
||||
const formatData = (list) => {
|
||||
return list.map(item => ({
|
||||
id: item.id,
|
||||
|
|
@ -454,19 +449,17 @@
|
|||
shopType: item.shoptype === 'B' ? '天猫' : '淘宝'
|
||||
}));
|
||||
};
|
||||
this.deserveList = formatData(res.data.data.deserve_lists);
|
||||
this.nineList = formatData(res.data.data.nine_lists);
|
||||
this.nineteenList = formatData(res.data.data.nineteen_lists);
|
||||
}
|
||||
}
|
||||
this.deserveList = formatData(res.data.deserve_lists);
|
||||
this.nineList = formatData(res.data.nine_lists);
|
||||
this.nineteenList = formatData(res.data.nineteen_lists);
|
||||
}).catch(err => {
|
||||
console.error('获取值得买列表失败:', err.message);
|
||||
});
|
||||
},
|
||||
getBrandSaleList() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/brandItem/choiceness?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data.brand_prefecture) {
|
||||
this.brandSaleShops = res.data.data.brand_prefecture.map(shop => ({
|
||||
http.get('https://api.cmspro.haodanku.com/brandItem/choiceness?cid=YsWZ21tx').then(res => {
|
||||
if (res.data.brand_prefecture) {
|
||||
this.brandSaleShops = res.data.brand_prefecture.map(shop => ({
|
||||
bg: shop.backimage || 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&q=80',
|
||||
logo: shop.brand_logo || 'https://images.unsplash.com/photo-1560155016-bd4879ae8f21?w=200&q=80',
|
||||
name: shop.fq_brand_name || '大牌特卖',
|
||||
|
|
@ -483,7 +476,8 @@
|
|||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取品牌特卖失败:', err.message);
|
||||
});
|
||||
},
|
||||
switchTab(index) {
|
||||
|
|
@ -1632,6 +1626,23 @@
|
|||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.price-coupon {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-val {
|
||||
font-size: 28rpx;
|
||||
color: #ff8a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.goods-sales {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@
|
|||
|
||||
<script>
|
||||
import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -194,18 +195,16 @@
|
|||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/ranking/lists',
|
||||
data: {
|
||||
http.get('https://api.cmspro.haodanku.com/ranking/lists', {
|
||||
type: this.rankType,
|
||||
category_id: this.currentCateId,
|
||||
page: this.page,
|
||||
page_size: 60,
|
||||
cid: 'YsWZ21tx'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && res.data.data.list_item && res.data.data.list_item.list) {
|
||||
const list = res.data.data.list_item.list.map(item => {
|
||||
})
|
||||
.then(res => {
|
||||
if (res.data && res.data.list_item && res.data.list_item.list) {
|
||||
const list = res.data.list_item.list.map(item => {
|
||||
// 处理标题
|
||||
let title = item.itemshorttitle || item.itemtitle;
|
||||
if (title.length > 18) {
|
||||
|
|
@ -235,10 +234,9 @@
|
|||
});
|
||||
this.goodsList = [...this.goodsList, ...list];
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
loadMore() {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@
|
|||
<text class="coupon-txt">{{ goods.couponValue }}元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-coupon" v-if="!$store.state.isThirdParty">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-tip">预估消费券</text>
|
||||
<text class="coupon-val">{{ $estimateCoupon(goods.tkmoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="goods-bottom-info">
|
||||
<text class="sales">已售{{ goods.sales }}件</text>
|
||||
|
|
@ -174,6 +180,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -250,22 +257,18 @@
|
|||
uni.setStorageSync('search_history', JSON.stringify(list));
|
||||
},
|
||||
getRankingList() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/search/searchRankingList?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data.ranking_list) {
|
||||
this.rankingList = res.data.data.ranking_list.slice(0, 10);
|
||||
}
|
||||
http.get('https://api.cmspro.haodanku.com/search/searchRankingList?cid=YsWZ21tx')
|
||||
.then(res => {
|
||||
if (res.data && res.data.ranking_list) {
|
||||
this.rankingList = res.data.ranking_list.slice(0, 10);
|
||||
}
|
||||
});
|
||||
},
|
||||
getHotThemes() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/index/hotTheme?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
this.hotThemes = res.data.data.slice(0, 10);
|
||||
}
|
||||
http.get('https://api.cmspro.haodanku.com/index/hotTheme?cid=YsWZ21tx')
|
||||
.then(res => {
|
||||
if (res.data) {
|
||||
this.hotThemes = res.data.slice(0, 10);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -317,11 +320,9 @@
|
|||
if (this.filterIsTmall) shopTypes.push(2);
|
||||
if (shopTypes.length > 0) data.shoptype = shopTypes.join(',');
|
||||
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/find/allItemList',
|
||||
data,
|
||||
success: (res) => {
|
||||
const body = res.data || {};
|
||||
http.get('https://api.cmspro.haodanku.com/find/allItemList', data)
|
||||
.then(res => {
|
||||
const body = res.body || {};
|
||||
if (body.code === 200 && body.data) {
|
||||
if (body.data.num_page) this.numPage = body.data.num_page;
|
||||
const rawList = body.data.item_info || [];
|
||||
|
|
@ -334,6 +335,7 @@
|
|||
: (item.itemshorttitle || item.itemtitle),
|
||||
finalPrice: item.itemendprice,
|
||||
couponValue: item.couponmoney || 0,
|
||||
tkmoney: item.tkmoney || 0,
|
||||
sales: item.itemsale >= 10000
|
||||
? (item.itemsale / 10000).toFixed(1) + '万'
|
||||
: item.itemsale,
|
||||
|
|
@ -359,13 +361,13 @@
|
|||
uni.showToast({ title: body.msg || '搜索失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '网络错误', icon: 'none' });
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.catch(err => {
|
||||
uni.showToast({ title: err.message || '网络错误', icon: 'none' });
|
||||
this.loading = false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
onScrollBottom() {
|
||||
|
|
@ -976,6 +978,23 @@
|
|||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.price-coupon {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-tip {
|
||||
font-size: 22rpx;
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-val {
|
||||
font-size: 28rpx;
|
||||
color: #ff8a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.goods-bottom-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -162,6 +162,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -443,11 +445,10 @@
|
|||
fetchTopBrandsData(isLoadMore = false) {
|
||||
if (this.topLoading) return;
|
||||
this.topLoading = true;
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/brandItem/getBrands?page=${this.topPage}&page_size=20&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.list)) {
|
||||
const list = res.data.data.list.map(item => ({
|
||||
http.get(`https://api.cmspro.haodanku.com/brandItem/getBrands?page=${this.topPage}&page_size=20&cid=YsWZ21tx`)
|
||||
.then((res) => {
|
||||
if (res.data && Array.isArray(res.data.list)) {
|
||||
const list = res.data.list.map(item => ({
|
||||
id: item.id,
|
||||
fq_brand_name: item.fq_brand_name,
|
||||
brand_logo: item.brand_logo ? item.brand_logo.replace('http://', 'https://') : ''
|
||||
|
|
@ -461,7 +462,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const pagination = res.data.data.pagination;
|
||||
const pagination = res.data.pagination;
|
||||
if (pagination && this.topPage >= pagination.page_count) {
|
||||
this.topFinished = true;
|
||||
} else if (list.length < 20) {
|
||||
|
|
@ -472,27 +473,25 @@
|
|||
this.topFinished = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('拉取上方滚动品牌接口异常', err);
|
||||
if (isLoadMore && this.topPage > 1) {
|
||||
this.topPage -= 1;
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.finally(() => {
|
||||
this.topLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 全量通用专场初始化及导航字典挂载
|
||||
fetchChoicenessData() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/brandItem/choiceness?is_get_category=1&cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data) {
|
||||
http.get('https://api.cmspro.haodanku.com/brandItem/choiceness?is_get_category=1&cid=YsWZ21tx')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
// 1. 挂载/补充导航字典
|
||||
if (res.data.data.category && Array.isArray(res.data.data.category)) {
|
||||
const mappedCats = res.data.data.category.map(c => ({
|
||||
if (res.data.category && Array.isArray(res.data.category)) {
|
||||
const mappedCats = res.data.category.map(c => ({
|
||||
cat_id: Number(c.cat_id),
|
||||
cat_name: c.cat_name
|
||||
}));
|
||||
|
|
@ -500,8 +499,8 @@
|
|||
}
|
||||
|
||||
// 2. 挂载全景精选海报流
|
||||
if (res.data.data.brand_prefecture && Array.isArray(res.data.data.brand_prefecture)) {
|
||||
const list = res.data.data.brand_prefecture.map(shop => {
|
||||
if (res.data.brand_prefecture && Array.isArray(res.data.brand_prefecture)) {
|
||||
const list = res.data.brand_prefecture.map(shop => {
|
||||
let logoStr = shop.brand_logo ? shop.brand_logo.replace('http://', 'https://') : 'https://cdn-icons-png.flaticon.com/512/882/882730.png';
|
||||
let bgStr = shop.backimage ? shop.backimage.replace('http://', 'https://') : 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&q=80';
|
||||
|
||||
|
|
@ -531,18 +530,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('拉取线上精选品牌专场分类联动数据失败,采用固定底座显示', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 接驳 brandCategory 接口分流,内置光影轮盘自动修复缺失背景。取消突兀 Toast 完美适配内嵌底座
|
||||
fetchBrandCategoryData(categoryId) {
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/brandItem/brandCategory?category_id=${categoryId}&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.brands)) {
|
||||
http.get(`https://api.cmspro.haodanku.com/brandItem/brandCategory?category_id=${categoryId}&cid=YsWZ21tx`)
|
||||
.then((res) => {
|
||||
if (res.data && Array.isArray(res.data.brands)) {
|
||||
// 预置唯美光影/极简质感图库底座,智能赋能无海报的品牌
|
||||
const defaultBanners = [
|
||||
'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&q=80',
|
||||
|
|
@ -552,7 +549,7 @@
|
|||
'https://images.unsplash.com/photo-1621939514649-280e2fc8a00w?w=800&q=80'
|
||||
];
|
||||
|
||||
const list = res.data.data.brands.map((b, bIndex) => {
|
||||
const list = res.data.brands.map((b, bIndex) => {
|
||||
let logoStr = b.brand_logo ? b.brand_logo.replace('http://', 'https://') : 'https://cdn-icons-png.flaticon.com/512/882/882730.png';
|
||||
let bgStr = defaultBanners[bIndex % defaultBanners.length];
|
||||
|
||||
|
|
@ -592,19 +589,17 @@
|
|||
} else {
|
||||
console.log('拉取分类数据异常或格式不对');
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('调用 brandCategory 接口失败', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 底部品牌热销单品聚合数据流
|
||||
fetchBrandSaleData() {
|
||||
uni.request({
|
||||
url: 'https://api.cmspro.haodanku.com/brandItem/brandSale?cid=YsWZ21tx',
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.items)) {
|
||||
const list = res.data.data.items.map(item => {
|
||||
http.get('https://api.cmspro.haodanku.com/brandItem/brandSale?cid=YsWZ21tx')
|
||||
.then((res) => {
|
||||
if (res.data && Array.isArray(res.data.items)) {
|
||||
const list = res.data.items.map(item => {
|
||||
let logoUrl = '';
|
||||
if (item.brand_info && item.brand_info.brand_logo) {
|
||||
logoUrl = item.brand_info.brand_logo.replace('http://', 'https://');
|
||||
|
|
@ -640,10 +635,9 @@
|
|||
this.brandSaleList = list;
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('拉取底部品牌热销接口异常,采用极尽精美的静态底座显示', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -278,21 +280,20 @@
|
|||
sortParam = this.priceOrder === 'asc' ? 8 : 9;
|
||||
}
|
||||
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/brandItem/detail?page=${this.page}&page_size=20&sort=${sortParam}&brand_id=${this.brandId}&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200 && res.data.data) {
|
||||
http.get(`https://api.cmspro.haodanku.com/brandItem/detail?page=${this.page}&page_size=20&sort=${sortParam}&brand_id=${this.brandId}&cid=YsWZ21tx`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
// 挂载核心品牌自画像
|
||||
if (res.data.data.brand_info) {
|
||||
const info = res.data.data.brand_info;
|
||||
if (res.data.brand_info) {
|
||||
const info = res.data.brand_info;
|
||||
if (info.brand_logo) info.brand_logo = info.brand_logo.replace('http://', 'https://');
|
||||
if (info.inside_logo) info.inside_logo = info.inside_logo.replace('http://', 'https://');
|
||||
this.brandInfo = info;
|
||||
}
|
||||
|
||||
// 挂载分页商品方阵
|
||||
if (res.data.data.items && Array.isArray(res.data.data.items.list)) {
|
||||
const list = res.data.data.items.list.map(goods => {
|
||||
if (res.data.items && Array.isArray(res.data.items.list)) {
|
||||
const list = res.data.items.list.map(goods => {
|
||||
let pic = goods.itempic ? goods.itempic.replace('http://', 'https://') : '';
|
||||
return {
|
||||
id: goods.id,
|
||||
|
|
@ -315,7 +316,7 @@
|
|||
this.itemList = list;
|
||||
}
|
||||
|
||||
const pagination = res.data.data.items.pagination;
|
||||
const pagination = res.data.items.pagination;
|
||||
if (pagination && this.page >= pagination.page_count) {
|
||||
this.finished = true;
|
||||
} else if (list.length < 20) {
|
||||
|
|
@ -327,15 +328,14 @@
|
|||
} else {
|
||||
uni.showToast({ title: '加载专区数据失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('拉取品牌专页接口错误', err);
|
||||
if (isLoadMore && this.page > 1) this.page -= 1;
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
if (!isLoadMore) uni.hideLoading();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* uni.request 统一封装
|
||||
*
|
||||
* 设计目标:
|
||||
* 1. 自动兼容项目中所有 API 的成功状态判断(code === 200 / code === 1 / status === 200)
|
||||
* 2. 支持自定义 header、超时、baseURL
|
||||
* 3. Promise 化,调用简单
|
||||
* 4. 支持请求/响应拦截器(便于后续统一注入 token、处理错误等)
|
||||
* 5. 数据提取自动降级:优先取 res.data.data,不存在则取 res.data
|
||||
*/
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @param {Object} config - 全局默认配置
|
||||
* @param {string} config.baseURL - 基础 URL
|
||||
* @param {number} config.timeout - 超时时间(ms),默认 30000
|
||||
* @param {Object} config.header - 默认请求头
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
baseURL: '',
|
||||
timeout: 30000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...config
|
||||
};
|
||||
this.interceptors = {
|
||||
request: [],
|
||||
response: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
* @param {Function} fn - (config) => config
|
||||
*/
|
||||
addRequestInterceptor(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this.interceptors.request.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应拦截器
|
||||
* @param {Function} fn - (response) => response
|
||||
*/
|
||||
addResponseInterceptor(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this.interceptors.response.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断响应是否成功
|
||||
*
|
||||
* 兼容项目中实际存在的 4 种响应格式:
|
||||
* 1. CMS API: res.data.code === 200
|
||||
* 2. Auth API: res.statusCode === 200 && (res.data.status === 200 || res.data.code === 1)
|
||||
* 3. Hdk v2 API: res.data.code === 1
|
||||
* 4. 兜底: res.statusCode === 200(无业务状态码时)
|
||||
*
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSuccess(res) {
|
||||
// HTTP 层已失败(如 404/500)
|
||||
if (res.statusCode >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
// 无响应体时,以 HTTP 状态码为准
|
||||
return res.statusCode === 200;
|
||||
}
|
||||
|
||||
// 提取业务状态码(兼容 code / status 两种字段名)
|
||||
const code = data.code !== undefined ? Number(data.code)
|
||||
: data.status !== undefined ? Number(data.status)
|
||||
: null;
|
||||
|
||||
if (code !== null && !isNaN(code)) {
|
||||
return code === 200 || code === 1;
|
||||
}
|
||||
|
||||
// 无业务状态码时,以 HTTP 状态码为准
|
||||
return res.statusCode === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取业务数据
|
||||
*
|
||||
* 降级策略:
|
||||
* - 优先取 res.data.data(CMS 标准格式)
|
||||
* - 不存在则取 res.data 本身(直接返回格式)
|
||||
* - 空响应返回 null
|
||||
*
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {any}
|
||||
*/
|
||||
extractData(res) {
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data;
|
||||
}
|
||||
return data.data !== undefined ? data.data : data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取错误信息
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {string}
|
||||
*/
|
||||
extractError(res) {
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return `请求失败 (HTTP ${res.statusCode})`;
|
||||
}
|
||||
return data.msg || data.message || data.error || `请求失败 (code: ${data.code || data.status})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心请求方法
|
||||
*
|
||||
* @param {Object} options - 请求配置
|
||||
* @param {string} options.url - 请求地址(可相对 baseURL)
|
||||
* @param {string} options.method - 请求方法:GET/POST/PUT/DELETE,默认 GET
|
||||
* @param {Object} options.data - 请求数据
|
||||
* @param {Object} options.header - 自定义请求头(会合并覆盖默认 header,大小写不敏感)
|
||||
* @param {number} options.timeout - 本次请求单独设置的超时时间
|
||||
* @returns {Promise<{success: true, data: any, body: any, raw: Object}>}
|
||||
*/
|
||||
request(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1. 合并配置:默认 → 实例配置 → 单次请求配置
|
||||
// header 合并需大小写不敏感去重(如 Content-Type 与 content-type 是同一头)
|
||||
const mergedHeader = {};
|
||||
for (const [k, v] of Object.entries(this.config.header)) {
|
||||
mergedHeader[k.toLowerCase()] = v;
|
||||
}
|
||||
for (const [k, v] of Object.entries(options.header || {})) {
|
||||
mergedHeader[k.toLowerCase()] = v;
|
||||
}
|
||||
let config = {
|
||||
...this.config,
|
||||
...options,
|
||||
header: mergedHeader
|
||||
};
|
||||
|
||||
// 2. 拼接 baseURL(如果 url 不是完整 HTTP 地址)
|
||||
if (config.baseURL && config.url && !config.url.startsWith('http')) {
|
||||
config.url = config.baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, '');
|
||||
}
|
||||
|
||||
// 3. 执行请求拦截器
|
||||
try {
|
||||
for (const fn of this.interceptors.request) {
|
||||
config = fn(config) || config;
|
||||
}
|
||||
} catch (err) {
|
||||
reject({ success: false, message: err.message || '请求拦截器异常', code: -2 });
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 发起请求
|
||||
uni.request({
|
||||
url: config.url,
|
||||
method: (config.method || 'GET').toUpperCase(),
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
timeout: config.timeout,
|
||||
success: (res) => {
|
||||
// 5. 执行响应拦截器
|
||||
try {
|
||||
for (const fn of this.interceptors.response) {
|
||||
res = fn(res) || res;
|
||||
}
|
||||
} catch (err) {
|
||||
reject({ success: false, message: err.message || '响应拦截器异常', code: -3, raw: res });
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 判断成功/失败
|
||||
if (this.isSuccess(res)) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: this.extractData(res),
|
||||
body: res.data,
|
||||
raw: res
|
||||
});
|
||||
} else {
|
||||
reject({
|
||||
success: false,
|
||||
message: this.extractError(res),
|
||||
code: res.data?.code ?? res.data?.status ?? res.statusCode,
|
||||
raw: res
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject({
|
||||
success: false,
|
||||
message: err.errMsg || '网络请求失败,请检查网络',
|
||||
code: -1,
|
||||
raw: err
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求快捷方法
|
||||
* @param {string} url
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
get(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'GET', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求快捷方法
|
||||
* @param {string} url
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
post(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'POST', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求快捷方法
|
||||
*/
|
||||
put(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'PUT', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求快捷方法
|
||||
*/
|
||||
delete(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'DELETE', ...options });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 创建全局默认实例
|
||||
// ============================================================
|
||||
const http = new Request();
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
export default http;
|
||||
export { Request };
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* uni.request 统一封装
|
||||
*
|
||||
* 设计目标:
|
||||
* 1. 自动兼容项目中所有 API 的成功状态判断(code === 200 / code === 1 / status === 200)
|
||||
* 2. 支持自定义 header、超时、baseURL
|
||||
* 3. Promise 化,调用简单
|
||||
* 4. 支持请求/响应拦截器(便于后续统一注入 token、处理错误等)
|
||||
* 5. 数据提取自动降级:优先取 res.data.data,不存在则取 res.data
|
||||
*/
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @param {Object} config - 全局默认配置
|
||||
* @param {string} config.baseURL - 基础 URL
|
||||
* @param {number} config.timeout - 超时时间(ms),默认 30000
|
||||
* @param {Object} config.header - 默认请求头
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
baseURL: '',
|
||||
timeout: 30000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...config
|
||||
};
|
||||
this.interceptors = {
|
||||
request: [],
|
||||
response: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
* @param {Function} fn - (config) => config
|
||||
*/
|
||||
addRequestInterceptor(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this.interceptors.request.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应拦截器
|
||||
* @param {Function} fn - (response) => response
|
||||
*/
|
||||
addResponseInterceptor(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
this.interceptors.response.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断响应是否成功
|
||||
*
|
||||
* 兼容项目中实际存在的 4 种响应格式:
|
||||
* 1. CMS API: res.data.code === 200
|
||||
* 2. Auth API: res.statusCode === 200 && (res.data.status === 200 || res.data.code === 1)
|
||||
* 3. Hdk v2 API: res.data.code === 1
|
||||
* 4. 兜底: res.statusCode === 200(无业务状态码时)
|
||||
*
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSuccess(res) {
|
||||
// HTTP 层已失败(如 404/500)
|
||||
if (res.statusCode >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
// 无响应体时,以 HTTP 状态码为准
|
||||
return res.statusCode === 200;
|
||||
}
|
||||
|
||||
// 提取业务状态码(兼容 code / status 两种字段名)
|
||||
const code = data.code !== undefined ? Number(data.code)
|
||||
: data.status !== undefined ? Number(data.status)
|
||||
: null;
|
||||
|
||||
if (code !== null && !isNaN(code)) {
|
||||
return code === 200 || code === 1;
|
||||
}
|
||||
|
||||
// 无业务状态码时,以 HTTP 状态码为准
|
||||
return res.statusCode === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取业务数据
|
||||
*
|
||||
* 降级策略:
|
||||
* - 优先取 res.data.data(CMS 标准格式)
|
||||
* - 不存在则取 res.data 本身(直接返回格式)
|
||||
* - 空响应返回 null
|
||||
*
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {any}
|
||||
*/
|
||||
extractData(res) {
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data;
|
||||
}
|
||||
return data.data !== undefined ? data.data : data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取错误信息
|
||||
* @param {Object} res - uni.request 的 success 回调参数
|
||||
* @returns {string}
|
||||
*/
|
||||
extractError(res) {
|
||||
const data = res.data;
|
||||
if (!data || typeof data !== 'object') {
|
||||
return `请求失败 (HTTP ${res.statusCode})`;
|
||||
}
|
||||
return data.msg || data.message || data.error || `请求失败 (code: ${data.code || data.status})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心请求方法
|
||||
*
|
||||
* @param {Object} options - 请求配置
|
||||
* @param {string} options.url - 请求地址(可相对 baseURL)
|
||||
* @param {string} options.method - 请求方法:GET/POST/PUT/DELETE,默认 GET
|
||||
* @param {Object} options.data - 请求数据
|
||||
* @param {Object} options.header - 自定义请求头(会合并覆盖默认 header,大小写不敏感)
|
||||
* @param {number} options.timeout - 本次请求单独设置的超时时间
|
||||
* @returns {Promise<{success: true, data: any, body: any, raw: Object}>}
|
||||
*/
|
||||
request(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1. 合并配置:默认 → 实例配置 → 单次请求配置
|
||||
// header 合并需大小写不敏感去重(如 Content-Type 与 content-type 是同一头)
|
||||
const mergedHeader = {};
|
||||
for (const [k, v] of Object.entries(this.config.header)) {
|
||||
mergedHeader[k.toLowerCase()] = v;
|
||||
}
|
||||
for (const [k, v] of Object.entries(options.header || {})) {
|
||||
mergedHeader[k.toLowerCase()] = v;
|
||||
}
|
||||
let config = {
|
||||
...this.config,
|
||||
...options,
|
||||
header: mergedHeader
|
||||
};
|
||||
|
||||
// 2. 拼接 baseURL(如果 url 不是完整 HTTP 地址)
|
||||
if (config.baseURL && config.url && !config.url.startsWith('http')) {
|
||||
config.url = config.baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, '');
|
||||
}
|
||||
|
||||
// 3. 执行请求拦截器
|
||||
try {
|
||||
for (const fn of this.interceptors.request) {
|
||||
config = fn(config) || config;
|
||||
}
|
||||
} catch (err) {
|
||||
reject({ success: false, message: err.message || '请求拦截器异常', code: -2 });
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 发起请求
|
||||
uni.request({
|
||||
url: config.url,
|
||||
method: (config.method || 'GET').toUpperCase(),
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
timeout: config.timeout,
|
||||
success: (res) => {
|
||||
// 5. 执行响应拦截器
|
||||
try {
|
||||
for (const fn of this.interceptors.response) {
|
||||
res = fn(res) || res;
|
||||
}
|
||||
} catch (err) {
|
||||
reject({ success: false, message: err.message || '响应拦截器异常', code: -3, raw: res });
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 判断成功/失败
|
||||
if (this.isSuccess(res)) {
|
||||
resolve({
|
||||
success: true,
|
||||
data: this.extractData(res),
|
||||
body: res.data,
|
||||
raw: res
|
||||
});
|
||||
} else {
|
||||
reject({
|
||||
success: false,
|
||||
message: this.extractError(res),
|
||||
code: res.data?.code ?? res.data?.status ?? res.statusCode,
|
||||
raw: res
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject({
|
||||
success: false,
|
||||
message: err.errMsg || '网络请求失败,请检查网络',
|
||||
code: -1,
|
||||
raw: err
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求快捷方法
|
||||
* @param {string} url
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
get(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'GET', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求快捷方法
|
||||
* @param {string} url
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
post(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'POST', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求快捷方法
|
||||
*/
|
||||
put(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'PUT', ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求快捷方法
|
||||
*/
|
||||
delete(url, data, options = {}) {
|
||||
return this.request({ url, data, method: 'DELETE', ...options });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 创建全局默认实例
|
||||
// ============================================================
|
||||
const http = new Request();
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
export default http;
|
||||
export { Request };
|
||||
|
|
@ -3,11 +3,15 @@ import { createStore } from 'vuex'
|
|||
const store = createStore({
|
||||
state: {
|
||||
currentUid: '',
|
||||
relationId: ''
|
||||
relationId: '',
|
||||
pid: '',
|
||||
isThirdParty: false
|
||||
},
|
||||
getters: {
|
||||
currentUid: state => state.currentUid,
|
||||
relationId: state => state.relationId
|
||||
relationId: state => state.relationId,
|
||||
pid: state => state.pid,
|
||||
isThirdParty: state => state.isThirdParty
|
||||
},
|
||||
mutations: {
|
||||
SET_CURRENT_UID(state, uid) {
|
||||
|
|
@ -21,6 +25,15 @@ const store = createStore({
|
|||
},
|
||||
CLEAR_RELATION_ID(state) {
|
||||
state.relationId = ''
|
||||
},
|
||||
SET_PID(state, pid) {
|
||||
state.pid = pid
|
||||
},
|
||||
CLEAR_PID(state) {
|
||||
state.pid = ''
|
||||
},
|
||||
SET_IS_THIRD_PARTY(state, flag) {
|
||||
state.isThirdParty = flag
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
|
@ -35,6 +48,15 @@ const store = createStore({
|
|||
},
|
||||
clearRelationId({ commit }) {
|
||||
commit('CLEAR_RELATION_ID')
|
||||
},
|
||||
setPid({ commit }, pid) {
|
||||
commit('SET_PID', pid)
|
||||
},
|
||||
clearPid({ commit }) {
|
||||
commit('CLEAR_PID')
|
||||
},
|
||||
setIsThirdParty({ commit }, flag) {
|
||||
commit('SET_IS_THIRD_PARTY', flag)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<view class="test-page">
|
||||
<view class="desc-card">
|
||||
<text class="title">IntersectionObserver 仅首次触发演示</text>
|
||||
<text class="subtitle">向下滚动,观察各模块首次进入视口时的淡入效果</text>
|
||||
</view>
|
||||
|
||||
<view class="section-title">场景:仅首次触发 — 视口内淡入动画</view>
|
||||
<view
|
||||
class="fade-box"
|
||||
v-for="(item, index) in fadeList"
|
||||
:key="index"
|
||||
:id="`fade-${index}`"
|
||||
:class="{ 'fade-in': item.visible }"
|
||||
>
|
||||
<text class="box-text">模块 {{ index + 1 }}</text>
|
||||
<text class="box-status">{{ item.triggered ? '✅ 已触发(不再重复)' : '⏳ 等待首次进入视口...' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="footer-space"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
fadeList: Array.from({ length: 10 }, () => ({
|
||||
visible: false,
|
||||
triggered: false
|
||||
})),
|
||||
// ⭐ 关键:用一个数组存放每个元素独立的 observer,避免互相覆盖
|
||||
observers: []
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
this.initFadeObserver();
|
||||
},
|
||||
onUnload() {
|
||||
// 页面卸载时,断开所有 observer
|
||||
this.observers.forEach(obs => obs.disconnect());
|
||||
},
|
||||
methods: {
|
||||
initFadeObserver() {
|
||||
this.fadeList.forEach((_, index) => {
|
||||
// ⭐ 核心修复:每个元素创建独立的 IntersectionObserver 实例
|
||||
// 同一个实例多次调用 observe() 会被覆盖,只有最后一个生效
|
||||
const observer = uni.createIntersectionObserver(this, {
|
||||
thresholds: [0]
|
||||
});
|
||||
|
||||
observer.relativeToViewport();
|
||||
|
||||
observer.observe(`#fade-${index}`, (result) => {
|
||||
const item = this.fadeList[index];
|
||||
|
||||
// 进入视口 且 之前没触发过
|
||||
if (result.intersectionRatio > 0 && !item.triggered) {
|
||||
console.log(`模块 ${index + 1} 首次进入视口`);
|
||||
|
||||
this.$set(this.fadeList, index, {
|
||||
visible: true,
|
||||
triggered: true
|
||||
});
|
||||
|
||||
// 触发后断开这个 observer,彻底释放
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 保存到数组,方便 onUnload 统一清理
|
||||
this.observers.push(observer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
background-color: #f5f6f8;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.desc-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 26rpx;
|
||||
margin-top: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 40rpx 0 20rpx;
|
||||
padding-left: 20rpx;
|
||||
border-left: 8rpx solid #ff416c;
|
||||
}
|
||||
|
||||
.fade-box {
|
||||
height: 240rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
opacity: 0;
|
||||
transform: translateY(60rpx);
|
||||
transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.fade-box.fade-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.box-text {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.box-status {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.footer-space {
|
||||
height: 100rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"hash": "0694fca5",
|
||||
"configHash": "6057985e",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "9518c2ac",
|
||||
"hash": "aee3647b",
|
||||
"configHash": "43aa957d",
|
||||
"lockfileHash": "32773baf",
|
||||
"browserHash": "732cb9f3",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// 预估消费券函数
|
||||
export function estimateCoupon(tkmoney = 0, percentage = 0.3) {
|
||||
let result = (tkmoney * percentage).toFixed(2);
|
||||
return result;
|
||||
}
|
||||
Loading…
Reference in New Issue