淘宝授权推送

This commit is contained in:
yankang 2026-06-02 14:59:50 +08:00
parent acd448b6dd
commit d1c0419d76
20 changed files with 1495 additions and 702 deletions

17
App.vue
View File

@ -3,6 +3,23 @@
export default {
globalData: {
/**
* urlParams - 当前页面 URL 参数集合
*
* 来源
* 1. H5 环境下从 window.locationsearch + 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) {

View File

@ -8,6 +8,8 @@
</template>
<script>
import http from '@/request/request.js';
export default {
props: {
activeTab: {
@ -25,16 +27,13 @@
},
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) {
//
const allowedTitles = ['首页', '榜单', '分类'];
this.bottomBarList = res.data.data.bottom_bar
.filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort);
}
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.bottom_bar
.filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort);
}
});
},

View File

@ -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
}

View File

@ -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,36 +157,29 @@
...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 || []);
this.category.list = block.map((item, index) => ({
label: item.name,
value: String(index)
}));
this.category.goodsLists = block.map(item => item.item || []);
const list = this.category.goodsLists[0] || [];
this.listData.list = list;
this.listData.finished = true;
} else {
this.listData.list = [];
this.listData.finished = true;
}
},
fail: () => {
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)
}));
this.category.goodsLists = block.map(item => item.item || []);
const list = this.category.goodsLists[0] || [];
this.listData.list = list;
this.listData.finished = true;
})
.catch(() => {
this.listData.list = [];
this.listData.finished = true;
},
complete: () => {
})
.finally(() => {
setTimeout(() => {
this.listData.loading = false;
this.loading = false;
}, 200);
}
});
});
},
reGetListData() {
@ -247,103 +241,68 @@
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;
const jumpUrl = data.coupon_click_url || data.item_url;
if (jumpUrl) {
uni.showModal({
title: '提示',
content: '是否跳转到下单页面?',
success: (modalRes) => {
if (modalRes.confirm) {
// #ifdef H5
window.location.href = jumpUrl;
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(jumpUrl);
// #endif
// #ifndef H5 || APP-PLUS
uni.setClipboardData({
data: jumpUrl,
success: () => {
uni.showToast({
title: '链接已复制,请在浏览器中打开',
icon: 'none',
duration: 3000
});
}
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({
title: '提示',
content: '是否跳转到下单页面?',
success: (modalRes) => {
if (modalRes.confirm) {
// #ifdef H5
window.location.href = jumpUrl;
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(jumpUrl);
// #endif
// #ifndef H5 || APP-PLUS
uni.setClipboardData({
data: jumpUrl,
success: () => {
uni.showToast({
title: '链接已复制,请在浏览器中打开',
icon: 'none',
duration: 3000
});
// #endif
}
}
});
});
// #endif
}
}
} 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;
const taoword = data.taoword;
if (taoword) {
const taocode = '0' + taoword + '/';
uni.setClipboardData({
data: taocode,
success: () => {
uni.showToast({
title: '复制口令成功,请打开淘宝领取',
icon: 'none',
duration: 2000
});
}
});
} else {
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 + '/';
uni.setClipboardData({
data: taocode,
success: () => {
uni.showToast({
title: '转链失败',
icon: 'none'
title: '复制口令成功,请打开淘宝领取',
icon: 'none',
duration: 2000
});
}
} else {
uni.showToast({
title: '转链失败',
icon: 'none'
});
}
},
fail: () => {
uni.showToast({
title: '转链失败',
icon: 'none'
});
} else {
uni.showToast({ title: '转链失败', icon: 'none' });
}
}).catch(() => {
uni.showToast({ title: '转链失败', icon: 'none' });
});
}
} catch (e) {

View File

@ -45,325 +45,335 @@
</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 {
data() {
return {
client_id: '30004725',
status: 'loading',
title: '正在完成授权...',
desc: '正在处理授权逻辑...',
CopyLink: false,
Token: '',
code: '',
queryState: '',
_t: null
export default {
data() {
return {
client_id: '30004725',
status: 'loading',
title: '正在完成授权...',
desc: '正在处理授权逻辑...',
CopyLink: false,
Token: '',
code: '',
queryState: '',
_t: null
}
},
computed: {
currentUid() {
return this.$store.getters.currentUid
}
},
onLoad() {
const app = getApp();
const params = app.globalData.urlParams || {};
this.Token = params.token || '';
this.code = params.code || '';
this.queryState = params.state || '';
this.exuid = params.exuid || '';
if(this.queryState) this.$store.dispatch('setCurrentUid', (params.state || '').replace(/^uid/, '') || '');
this.handleAuth();
},
onUnload() {
if (this._t) {
clearTimeout(this._t);
this._t = null;
}
},
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';
// // #endif
},
handleAuth() {
const baseUrl = this.getBaseUrl();
if (this.Token) {
// 1 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) {
// 3uid
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('授权链接异常,请重新授权!');
}
},
computed: {
currentUid() {
return this.$store.getters.currentUid
handleUserInfo(res) {
const data = res.data || {};
const relationId = Number(data.relation_id);
const uid = data.uid;
const pid = data.tbk_pid;
if (relationId) {
// pid使
this.setAuthData(relationId, pid);
//
const app = getApp();
const params = app.globalData.urlParams || {};
if (params?.page_uri && this.isValidPagePath(params.page_uri)) {
uni.navigateTo({
url: params.page_uri
});
} else {
this.goHome();
}
} else if (uid) {
//
this.$store.dispatch('setCurrentUid', uid);
this.Environment();
} else {
this.showFail('获取用户信息失败!');
}
},
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.handleAuth();
handleUserError(err) {
console.error('获取用户信息失败:', err);
const msg = err.raw?.data?.msg || err.raw?.data?.message || err.message || '获取用户信息失败,请检查网络';
this.showFail(msg);
},
onUnload() {
setAuthData(relationId, pid) {
this.$store.dispatch('setRelationId', relationId);
this.$store.dispatch('setPid', pid || 'mm_284380119_1881450385_111415850448');
},
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() {
if (this._t) {
clearTimeout(this._t);
this._t = null;
};
uni.reLaunch({
url: '/pages/index/index'
});
// const url = '/affiliate-activity/pages/index/index';
// // #ifdef H5
// window.location.href = url;
// return;
// // #endif
// uni.reLaunch({
// url: url
// });
},
showFail(message) {
this.status = 'fail';
this.title = '授权失败';
this.desc = message || '请重新授权或返回首页';
},
showSuccess(message) {
this.status = 'success';
this.title = '授权成功';
this.desc = message;
// this._t = setTimeout(() => this.goHome(), 500);
this.goHome()
},
CopyStatus(message) {
this.status = 'CopyUrl';
this.title = '';
this.desc = message || '请点击下方按钮复制授权地址';
},
Environment() {
const env = getEnvironment();
const uid = `uid${this.currentUid}`;
const redirectUri = this.getRedirectUri();
const authUrl = `https://oauth.taobao.com/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${uid}&view=wap`;
if (env.isBrowser || env.isAppWebview) {
// /APP WebView
// #ifdef H5
window.location.href = authUrl;
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(authUrl);
// #endif
} else if (env.isWechatWebview || env.isWechatH5 || env.isWechatMiniProgram) {
// /
this.CopyStatus('请复制授权地址到浏览器打开');
this.CopyLink = true;
} else {
//
this.CopyStatus('请复制授权地址到浏览器打开');
this.CopyLink = true;
}
},
methods: {
getBaseUrl() {
return window.location.origin;
// // #ifdef H5
// return window.location.origin;
// // #endif
// // #ifndef H5
// // H5 /App H5
// return 'https://tpoint.agrimedia.cn';
// // #endif
},
handleAuth() {
const baseUrl = this.getBaseUrl();
if (this.Token) {
// 1 token
uni.request({
url: baseUrl + '/api/user',
header: {
'authori-zation': `Bearer ${this.Token}`
},
success: (res) => {
if (res.statusCode === 200 && res.data) {
const data = res.data.data || {};
const relationId = data.relation_id;
const uid = data.uid;
if (relationId) {
//
this.$store.dispatch('setRelationId', relationId);
const app = getApp();
const params = app.globalData.urlParams || {};
if(params.page_uri) {
uni.navigateTo({
url: params.page_uri
});
}else{
this.goHome();
}
} else if (uid) {
//
this.$store.dispatch('setCurrentUid', uid);
this.Environment();
} else {
this.showFail('获取用户信息失败!');
}
} else {
this.showFail(res.data?.msg || res.data?.message || '获取用户信息失败');
}
},
fail: (err) => {
console.error('获取用户信息失败:', err);
this.showFail('获取用户信息失败,请检查网络');
}
copyText() {
const uid = `uid${this.currentUid}`;
const redirectUri = this.getRedirectUri();
const text = `https://oauth.taobao.com/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${uid}&view=wap`;
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '授权链接已复制',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '复制失败,请手动复制',
icon: 'none'
});
} else if (this.code && this.queryState) {
// 2 code + state
uni.request({
url: baseUrl + '/api/taobao/execute',
data: {
code: this.code,
state: this.queryState
},
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('授权处理失败,请重新授权');
}
},
fail: (err) => {
console.error('授权失败:', err);
this.showFail('授权处理失败,请重新授权');
}
});
} else {
this.showFail('授权链接异常,请重新授权!');
// this.$store.dispatch('setRelationId', '123456789');
// this.showSuccess('...');
}
},
goHome() {
if (this._t) {
clearTimeout(this._t);
this._t = null;
};
uni.reLaunch({
url: '/pages/index/index'
});
// const url = '/affiliate-activity/pages/index/index';
// // #ifdef H5
// window.location.href = url;
// return;
// // #endif
// uni.reLaunch({
// url: url
// });
},
showFail(message) {
this.status = 'fail';
this.title = '授权失败';
this.desc = message || '请重新授权或返回首页';
},
showSuccess(message) {
this.status = 'success';
this.title = '授权成功';
this.desc = message;
this._t = setTimeout(() => this.goHome(), 500);
},
CopyStatus(message) {
this.status = 'CopyUrl';
this.title = '';
this.desc = message || '请点击下方按钮复制授权地址';
},
Environment() {
const env = getEnvironment();
const uid = `uid${this.currentUid}`;
const redirectUri = this.getRedirectUri();
const authUrl = `https://oauth.taobao.com/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${uid}&view=wap`;
if (env.isBrowser || env.isAppWebview) {
// /APP WebView
// #ifdef H5
window.location.href = authUrl;
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(authUrl);
// #endif
} else if (env.isWechatWebview || env.isWechatH5 || env.isWechatMiniProgram) {
// /
this.CopyStatus('请复制授权地址到浏览器打开');
this.CopyLink = true;
} else {
//
this.CopyStatus('请复制授权地址到浏览器打开');
this.CopyLink = true;
}
},
});
},
copyText() {
const uid = `uid${this.currentUid}`;
const redirectUri = this.getRedirectUri();
const text = `https://oauth.taobao.com/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${uid}&view=wap`;
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '授权链接已复制',
icon: 'success'
});
},
fail: () => {
uni.showToast({
title: '复制失败,请手动复制',
icon: 'none'
});
}
});
},
getRedirectUri() {
const baseUrl = this.getBaseUrl();
return baseUrl + '/affiliate-activity/pages/auth/auth';
},
}
getRedirectUri() {
const baseUrl = this.getBaseUrl();
return baseUrl + '/affiliate-activity/pages/auth/auth';
},
}
}
</script>
<style scoped>
.auth-page {
min-height: 100vh;
background: linear-gradient(180deg, #f0f2f5 0%, #f7f8fa 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
.auth-page {
min-height: 100vh;
background: linear-gradient(180deg, #f0f2f5 0%, #f7f8fa 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
}
.auth-box {
background: #ffffff;
border-radius: 24rpx;
padding: 100rpx 56rpx 80rpx;
width: 100%;
max-width: 640rpx;
text-align: center;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.06);
}
.auth-icon {
margin: 0 auto 40rpx;
display: flex;
align-items: center;
justify-content: center;
height: 100rpx;
}
.spin-animation {
animation: spin 1.2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
.auth-box {
background: #ffffff;
border-radius: 24rpx;
padding: 100rpx 56rpx 80rpx;
width: 100%;
max-width: 640rpx;
text-align: center;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.06);
100% {
transform: rotate(360deg);
}
}
.auth-icon {
margin: 0 auto 40rpx;
display: flex;
align-items: center;
justify-content: center;
height: 100rpx;
}
.auth-title {
font-size: 44rpx;
color: #1a1a1a;
margin: 24rpx 0 20rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.spin-animation {
animation: spin 1.2s linear infinite;
}
.auth-desc {
font-size: 28rpx;
color: #666666;
margin-bottom: 56rpx;
line-height: 1.8;
padding: 0 20rpx;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
.auth-btn {
margin-top: 32rpx;
height: 96rpx;
line-height: 96rpx;
background: #ffffff;
color: #333333;
border: 2rpx solid #e0e0e0;
border-radius: 48rpx;
font-size: 30rpx;
padding: 0 48rpx;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12rpx;
transition: all 0.2s ease;
}
100% {
transform: rotate(360deg);
}
}
.auth-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.auth-title {
font-size: 44rpx;
color: #1a1a1a;
margin: 24rpx 0 20rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.auth-btn::after {
border: none;
}
.auth-desc {
font-size: 28rpx;
color: #666666;
margin-bottom: 56rpx;
line-height: 1.8;
padding: 0 20rpx;
}
.auth-btn--primary {
background: linear-gradient(135deg, #ff715a, #ff416c);
color: #ffffff;
border: none;
box-shadow: 0 8rpx 24rpx rgba(255, 65, 108, 0.25);
}
.auth-btn {
margin-top: 32rpx;
height: 96rpx;
line-height: 96rpx;
background: #ffffff;
color: #333333;
border: 2rpx solid #e0e0e0;
border-radius: 48rpx;
font-size: 30rpx;
padding: 0 48rpx;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12rpx;
transition: all 0.2s ease;
}
.auth-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.auth-btn::after {
border: none;
}
.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 {
color: inherit;
font-size: 30rpx;
font-weight: 500;
}
.btn-text {
color: inherit;
font-size: 30rpx;
font-weight: 500;
}
</style>

View File

@ -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,22 +161,19 @@
});
},
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) {
// navList
const categories = res.data.data.map(item => ({
name: item.name,
cat_id: item.cat_id,
second: item.second || []
}));
this.navList = [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }, ...categories];
const currentCat = this.navList.find(c => c.cat_id == this.currentCatId);
if (currentCat && currentCat.second) {
this.subCategories = currentCat.second;
}
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.map(item => ({
name: item.name,
cat_id: item.cat_id,
second: item.second || []
}));
this.navList = [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }, ...categories];
const currentCat = this.navList.find(c => c.cat_id == this.currentCatId);
if (currentCat && currentCat.second) {
this.subCategories = currentCat.second;
}
}
});
@ -191,40 +195,37 @@
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 => ({
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,
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale,
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
shopName: item.shopname,
labels: item.label || []
}));
if (list.length === 0) {
this.finished = true;
} else {
//
this.goodsList = [...this.goodsList, ...list];
this.page++;
//
if (list.length < 20) {
this.finished = true;
}
}
} else {
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,
labels: item.label || []
}));
if (list.length === 0) {
this.finished = true;
} else {
//
this.goodsList = [...this.goodsList, ...list];
this.page++;
//
if (list.length < 20) {
this.finished = true;
}
}
},
complete: () => {
this.loading = false;
} else {
this.finished = true;
}
}).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;

View File

@ -17,7 +17,7 @@
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore">
<!-- 占位适配固定头部 -->
<view class="header-placeholder" :style="{ height: '44px' }"></view>
<!-- 排序筛选栏 -->
<view class="filter-bar">
<view class="filter-item" :class="{ 'active': sortType === 1 }" @click="changeSort(1)">综合</view>
@ -33,28 +33,35 @@
<!-- 高级筛选标签栏 -->
<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">
<view class="price-main">
<text class="price-tip">{{ goods.couponValue > 0 ? '券后' : '抢购价' }}</text>
@ -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>
@ -122,13 +144,14 @@
<view v-else-if="goodsList.length === 0" class="empty-text">暂无相关商品</view>
<view v-else-if="finished" class="finished-text">-- 已经到底啦 --</view>
</view>
<view class="footer-placeholder"></view>
</scroll-view>
</view>
</template>
<script>
import http from '@/request/request.js';
export default {
data() {
return {
@ -154,11 +177,11 @@
onLoad(options) {
const sysInfo = uni.getSystemInfoSync();
this.statusBarHeight = sysInfo.statusBarHeight || 44;
if (options.cate_name) this.cateName = decodeURIComponent(options.cate_name);
if (options.id) this.mainCatId = options.id;
if (options.second_category) this.secondCategory = options.second_category;
this.getProducts(true);
},
methods: {
@ -172,7 +195,7 @@
this.goodsList = [];
}
if (this.loading || this.finished) return;
this.loading = true;
// (0:, 3:, 8:, 9:)
let sortParam = 0;
@ -186,7 +209,7 @@
let filterParams = '';
if (this.filterHasCoupon) filterParams += '&filtrate_type=16';
if (this.filterIsBrand) filterParams += '&is_brand=1';
// shoptype: 121,2
let shopTypes = [];
if (this.filterIsFlagship) shopTypes.push(1);
@ -195,44 +218,45 @@
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 => ({
id: item.id,
image: item.itempic,
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,
shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
shopName: item.shopname,
labels: item.label || []
}));
if (list.length === 0) {
this.finished = true;
} else {
this.goodsList = [...this.goodsList, ...list];
this.page++;
if (list.length < 20) {
this.finished = true;
}
}
// 10
if (refresh && this.goodsList.length < 10) {
this.getRecommendList();
}
} else {
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,
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,
labels: item.label || []
}));
if (list.length === 0) {
this.finished = true;
if (refresh) this.getRecommendList();
} else {
this.goodsList = [...this.goodsList, ...list];
this.page++;
if (list.length < 20) {
this.finished = true;
}
}
},
complete: () => {
this.loading = false;
// 10
if (refresh && this.goodsList.length < 10) {
this.getRecommendList();
}
} else {
this.finished = true;
if (refresh) this.getRecommendList();
}
}).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)
return {
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: () => {
this.loadingRecommend = false;
});
this.recommendList = list;
}
}).finally(() => {
this.loadingRecommend = false;
});
},
changeSort(type) {
@ -284,13 +311,13 @@
if (type === 'flagship') this.filterIsFlagship = !this.filterIsFlagship;
if (type === 'tmall') this.filterIsTmall = !this.filterIsTmall;
if (type === 'brand') this.filterIsBrand = !this.filterIsBrand;
this.getProducts(true);
},
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;
@ -609,4 +653,4 @@
.footer-placeholder {
height: 40rpx;
}
</style>
</style>

View File

@ -47,6 +47,7 @@
<script>
import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
import http from '@/request/request.js';
export default {
components: {
@ -75,35 +76,28 @@
},
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;
//
this.matchNavActive();
}
}
}).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) {
const allowedTitles = ['首页', '榜单', '分类'];
const list = res.data.data.bottom_bar
.filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort);
const idx = list.findIndex(item => item.title === '分类');
if (idx !== -1) {
this.navActiveIndex = idx;
}
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.bottom_bar
.filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort);
const idx = list.findIndex(item => item.title === '分类');
if (idx !== -1) {
this.navActiveIndex = idx;
}
}
});

View File

@ -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;

View File

@ -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,20 +423,18 @@
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;
}
}
});
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) {
const formatData = (list) => {
http.get('https://api.cmspro.haodanku.com/index/deserveLists?cid=YsWZ21tx').then(res => {
const formatData = (list) => {
return list.map(item => ({
id: item.id,
img: item.itempic,
@ -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 || '大牌特卖',
@ -481,9 +474,10 @@
price: goods.itemendprice,
coupon: goods.couponmoney
}))
}));
}
}));
}
}).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;

View File

@ -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,11 +234,10 @@
});
this.goodsList = [...this.goodsList, ...list];
}
},
complete: () => {
})
.finally(() => {
this.loading = false;
}
});
});
},
loadMore() {
// 60

View File

@ -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,24 +257,20 @@
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);
}
}
});
});
},
onInputFocus() {
//
@ -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,14 +361,14 @@
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() {
if (this.isSearching && !this.finished && !this.loading && this.currentKeyword) {
@ -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;

View File

@ -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,11 +635,10 @@
this.brandSaleList = list;
}
}
},
fail: (err) => {
})
.catch((err) => {
console.log('拉取底部品牌热销接口异常,采用极尽精美的静态底座显示', err);
}
});
});
}
}
}

View File

@ -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,16 +328,15 @@
} 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();
}
});
});
}
}
}

257
request/request.js Normal file
View File

@ -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.dataCMS 标准格式
* - 不存在则取 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 };

257
scratch/test_request.mjs Normal file
View File

@ -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.dataCMS 标准格式
* - 不存在则取 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 };

View File

@ -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)
}
}
})

150
test.vue Normal file
View File

@ -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>

View File

@ -1,8 +1,8 @@
{
"hash": "0694fca5",
"configHash": "6057985e",
"lockfileHash": "e3b0c442",
"browserHash": "9518c2ac",
"hash": "aee3647b",
"configHash": "43aa957d",
"lockfileHash": "32773baf",
"browserHash": "732cb9f3",
"optimized": {},
"chunks": {}
}

5
utils/index.js Normal file
View File

@ -0,0 +1,5 @@
// 预估消费券函数
export function estimateCoupon(tkmoney = 0, percentage = 0.3) {
let result = (tkmoney * percentage).toFixed(2);
return result;
}