Compare commits
2 Commits
4aa20b7257
...
d1c0419d76
| Author | SHA1 | Date |
|---|---|---|
|
|
d1c0419d76 | |
|
|
acd448b6dd |
56
App.vue
56
App.vue
|
|
@ -1,13 +1,59 @@
|
|||
<script>
|
||||
import UrlQuery from '@/scratch/url-query.js';
|
||||
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
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: {}
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
onLaunch: function(options) {
|
||||
console.log('App Launch');
|
||||
|
||||
// 获取 URL 参数并存入 globalData
|
||||
let params = UrlQuery.getAll();
|
||||
|
||||
// #ifndef H5
|
||||
// 非 H5 平台,合并 App.onLaunch 的 options.query(分享/扫码进入时的参数)
|
||||
if (options && options.query && typeof options.query === 'object') {
|
||||
params = { ...params, ...options.query };
|
||||
}
|
||||
// #endif
|
||||
|
||||
this.globalData.urlParams = params;
|
||||
console.log('URL 参数:', params);
|
||||
},
|
||||
onShow: function(options) {
|
||||
console.log('App Show');
|
||||
|
||||
// 每次显示时更新参数(H5 下 URL 可能变化)
|
||||
let params = UrlQuery.getAll();
|
||||
|
||||
// #ifndef H5
|
||||
if (options && options.query && typeof options.query === 'object') {
|
||||
params = { ...params, ...options.query };
|
||||
}
|
||||
// #endif
|
||||
|
||||
this.globalData.urlParams = params;
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
console.log('App Hide');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
6
main.js
6
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
|
||||
|
|
@ -13,8 +14,11 @@ app.$mount()
|
|||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
import store from './store'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(store)
|
||||
app.config.globalProperties.$estimateCoupon = estimateCoupon;
|
||||
return {
|
||||
app
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name" : "baimacms",
|
||||
"appid" : "__UNI__404B6E1",
|
||||
"appid" : "__UNI__924A228",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
|
|
@ -68,5 +68,11 @@
|
|||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
"vueVersion" : "3",
|
||||
"h5" : {
|
||||
"router" : {
|
||||
"mode" : "history",
|
||||
"base" : "/affiliate-activity/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
{
|
||||
"name": "baimacms",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "baimacms",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-ui": "^1.5.12",
|
||||
"vuex": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz",
|
||||
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dcloudio/uni-ui": {
|
||||
"version": "1.5.12",
|
||||
"resolved": "https://registry.npmmirror.com/@dcloudio/uni-ui/-/uni-ui-1.5.12.tgz",
|
||||
"integrity": "sha512-mGDl2OZSz7D8xcUAzJegWDHOqB4MEFBSW9Esb/oJiu2/3Gk9+P/Z4bA4JZ9jv9VWBYbMrYwaTfK1Z728kABdYg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
|
||||
"integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/shared": "3.5.34",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz",
|
||||
"integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz",
|
||||
"integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/compiler-core": "3.5.34",
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-ssr": "3.5.34",
|
||||
"@vue/shared": "3.5.34",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.14",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz",
|
||||
"integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz",
|
||||
"integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz",
|
||||
"integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz",
|
||||
"integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.34",
|
||||
"@vue/runtime-core": "3.5.34",
|
||||
"@vue/shared": "3.5.34",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz",
|
||||
"integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz",
|
||||
"integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.34",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz",
|
||||
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-sfc": "3.5.34",
|
||||
"@vue/runtime-dom": "3.5.34",
|
||||
"@vue/server-renderer": "3.5.34",
|
||||
"@vue/shared": "3.5.34"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vuex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.1.0.tgz",
|
||||
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.0.0-beta.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "baimacms",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.agrimedia.cn/15133400227/baimacms.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-ui": "^1.5.12",
|
||||
"vuex": "^4.1.0"
|
||||
}
|
||||
}
|
||||
51
pages.json
51
pages.json
|
|
@ -1,63 +1,94 @@
|
|||
{
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "首页",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/detail/detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "商品详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/category/category",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "商品分类"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/category/category_detail",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "商品分类详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/rank/rank",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "商品排名"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/classify/classify",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "商品分类列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/search/search",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "搜索"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/special-sale/choicen",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "特殊销售"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/special-sale/details",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "特殊销售详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/save-money/save-money",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "省钱券"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/auth",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "授权"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/activity",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,634 @@
|
|||
<template>
|
||||
<view class="activity-page">
|
||||
<!-- Banner -->
|
||||
<view class="banner-wrapper">
|
||||
<image class="banner-img" :src="banner" mode="widthFix" />
|
||||
</view>
|
||||
|
||||
<!-- 分类 Tabs -->
|
||||
<view class="category-sticky" v-if="category.list.length > 1">
|
||||
<scroll-view scroll-x class="category-scroll" :show-scrollbar="false">
|
||||
<view class="category-tabs">
|
||||
<view v-for="item in category.list" :key="item.value" class="tab-item"
|
||||
:class="{ active: category.value === item.value }" @click="onNavChange(item.value)">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="goods-list" :class="{ 'empty-list': !listData.list.length }">
|
||||
<view class="goods-card" v-for="(item, index) in listData.list" :key="item.itemid"
|
||||
@click="handleGoodsTransfer(item)">
|
||||
<view class="cover img-box">
|
||||
<image class="goods-img" :src="item.itempic" mode="aspectFill" lazy-load v-if="item.itempic" />
|
||||
<image class="goods-img" src="http://img.bc.fqapps.com/fudai13cae4ae6ef16739ed3b100a2ec39e97.gif"
|
||||
mode="aspectFill" v-else />
|
||||
<view class="sale-tag">
|
||||
<text class="fire-icon">🔥</text>
|
||||
<text>日销量 {{ getMoneyStr(item.itemsale2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail">
|
||||
<view class="title">
|
||||
<image v-if="item.shoptype === 'B'" class="shop-icon tm"
|
||||
src="http://img-haodanku-com.cdn.fudaiapp.com/FvU9Lg74-WgBzWQphv98-9KgYF6d" />
|
||||
<image v-else-if="item.shoptype === 'C'" class="shop-icon tb"
|
||||
src="http://img-haodanku-com.cdn.fudaiapp.com/FnCy1eY4W5khYdTw_iIDhKuwGLmu" />
|
||||
<text class="title-text">{{ item.itemshorttitle || item.itemtitle }}</text>
|
||||
</view>
|
||||
<view class="center">
|
||||
<text>{{ item.itemdesc }}</text>
|
||||
</view>
|
||||
<view class="price">
|
||||
<view class="end-price">
|
||||
券后价<text class="price-strong">¥{{ Number(item.itemendprice) }}</text>
|
||||
</view>
|
||||
<view class="origin">
|
||||
<text class="del-price">¥{{ Number(item.itemprice) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tags" v-if="item.couponmoney != '0'">
|
||||
<view class="ticket">
|
||||
<text class="name">券</text><text class="value">¥{{ item.couponmoney }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-block">
|
||||
<view class="btn-order" @click.stop="handleGoodsTransfer(item)">
|
||||
<text>立即购买</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="list-loading" v-if="loading && !listData.list.length">数据正在赶来的路上...</view>
|
||||
<view class="list-finished" v-if="listData.finished && listData.list.length">-优惠到底啦-</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享按钮 -->
|
||||
<view class="share-btn" :class="{ hide: isScrollToDown }" @click="handleShare">
|
||||
<view class="share-icon">
|
||||
<svg viewBox="0 0 18 19.11" class="svg-share">
|
||||
<path class="cls-1"
|
||||
d="M17.5,8A2.5,2.5,0,1,0,15,5.5,2.5,2.5,0,0,0,17.5,8Z" transform="translate(-3 -2)" />
|
||||
<path class="cls-1"
|
||||
d="M6.5,14.5A2.5,2.5,0,1,0,4,12,2.5,2.5,0,0,0,6.5,14.5Z" transform="translate(-3 -2)" />
|
||||
<path class="cls-2" d="M15,6.79,8.67,10.62" transform="translate(-3 -2)" />
|
||||
<path class="cls-2" d="M8.67,13.28,13.27,16" transform="translate(-3 -2)" />
|
||||
<path class="cls-1"
|
||||
d="M15.77,15.11a2.5,2.5,0,1,1-2.5,2.5A2.5,2.5,0,0,1,15.77,15.11Z" transform="translate(-3 -2)" />
|
||||
</svg>
|
||||
</view>
|
||||
<text>分享好友</text>
|
||||
</view>
|
||||
|
||||
<!-- 回到顶部 -->
|
||||
<view class="back-top-btn" :class="{ hide: scrollTop < 600 }" @click="backTop"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import http from '@/request/request.js';
|
||||
const API_BASE = 'https://v2.api.haodanku.com';
|
||||
const CUSTOM_PARAMS = {
|
||||
apikey: '5417B681C5EA'
|
||||
};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
banner: '',
|
||||
loading: false,
|
||||
scrollTop: 0,
|
||||
lastScrollTop: 0,
|
||||
isScrollToDown: false,
|
||||
category: {
|
||||
value: '0',
|
||||
list: [],
|
||||
goodsLists: []
|
||||
},
|
||||
listData: {
|
||||
list: [],
|
||||
loading: false,
|
||||
finished: false
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
const stamp = Date.now() - Date.now() % (60 * 1000 * 30);
|
||||
this.banner = 'https://img.bc.haodanku.com/cms/1635994050?t=' + stamp;
|
||||
this.getListData();
|
||||
},
|
||||
onPageScroll(e) {
|
||||
const st = e.scrollTop;
|
||||
this.isScrollToDown = st > this.lastScrollTop;
|
||||
this.lastScrollTop = st;
|
||||
this.scrollTop = st;
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.reGetListData().finally(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getListDataParams() {
|
||||
return { ...CUSTOM_PARAMS };
|
||||
},
|
||||
getTransferParams() {
|
||||
const rid = this.$store.getters.relationId;
|
||||
return {
|
||||
pid: 'mm_284380119_1881450385_111415850448',
|
||||
tb_name: 'michuan2018',
|
||||
relation_id: rid || '-1',
|
||||
...CUSTOM_PARAMS
|
||||
};
|
||||
},
|
||||
|
||||
async getListData() {
|
||||
const listData = this.listData;
|
||||
if (this.loading || listData.loading || listData.finished) return;
|
||||
listData.loading = true;
|
||||
this.loading = true;
|
||||
|
||||
const params = {
|
||||
id: 125,
|
||||
...this.getListDataParams()
|
||||
};
|
||||
|
||||
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;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.listData.loading = false;
|
||||
this.loading = false;
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
|
||||
reGetListData() {
|
||||
this.listData.list = [];
|
||||
this.listData.finished = false;
|
||||
return this.getListData();
|
||||
},
|
||||
|
||||
onNavChange(value) {
|
||||
this.category.value = value;
|
||||
this.listData.list = this.category.goodsLists[value] || [];
|
||||
},
|
||||
|
||||
backTop() {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 300
|
||||
});
|
||||
},
|
||||
|
||||
handleShare() {
|
||||
let shareUrl;
|
||||
// #ifdef H5
|
||||
shareUrl = window.location.href;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
shareUrl = 'https://your-domain.com/pages/activity/activity';
|
||||
// #endif
|
||||
|
||||
uni.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '复制成功,分享给身边好友吧~',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async handleGoodsTransfer(item) {
|
||||
const param = {
|
||||
itemid: item.itemid,
|
||||
title: item.itemshorttitle || item.itemtitle,
|
||||
get_taoword: 1,
|
||||
...this.getTransferParams()
|
||||
};
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
let isWechatEnv;
|
||||
// #ifdef H5
|
||||
isWechatEnv = /micromessenger/i.test(navigator.userAgent);
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
isWechatEnv = false;
|
||||
// #endif
|
||||
|
||||
if (!isWechatEnv) {
|
||||
// 非微信环境:弹窗确认后跳转
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
});
|
||||
} 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',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
}
|
||||
}).catch(() => {
|
||||
uni.showToast({ title: '转链失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: '转链失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.loading = false;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
getMoneyStr(num) {
|
||||
const n = Number(num);
|
||||
const w = 10000;
|
||||
if (n < w) return String(n);
|
||||
return (n / w).toFixed(2) + '万';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-page {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.banner-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.category-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.category-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
padding: 0 20rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 24rpx 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #ff416c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #ff416c;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.goods-list.empty-list {
|
||||
min-height: 400rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: relative;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sale-tag {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to right, rgba(255, 65, 108, 0.9), rgba(255, 75, 43, 0.9));
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fire-icon {
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.detail {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.shop-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.center {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.end-price {
|
||||
font-size: 24rpx;
|
||||
color: #ff416c;
|
||||
}
|
||||
|
||||
.price-strong {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.origin {
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.del-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.ticket {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1rpx solid #ff416c;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.ticket .name {
|
||||
background: linear-gradient(to right, #ff715a, #ff416c);
|
||||
color: #fff;
|
||||
padding: 2rpx 10rpx;
|
||||
}
|
||||
|
||||
.ticket .value {
|
||||
color: #ff416c;
|
||||
padding: 2rpx 10rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
margin-top: 12rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-order {
|
||||
background: linear-gradient(to right, #ff715a, #ff416c);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 32rpx;
|
||||
border-radius: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-loading,
|
||||
.list-finished {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
bottom: 160rpx;
|
||||
z-index: 200;
|
||||
background: rgba(255, 65, 108, 0.9);
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 65, 108, 0.3);
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
.share-btn.hide {
|
||||
transform: translateX(120%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.svg-share {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.svg-share .cls-1 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.svg-share .cls-2 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.back-top-btn {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
bottom: 80rpx;
|
||||
z-index: 200;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
.back-top-btn::before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-bottom: 20rpx solid #fff;
|
||||
}
|
||||
|
||||
.back-top-btn.hide {
|
||||
transform: translateY(200%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
<template>
|
||||
<view class="auth-page">
|
||||
<view class="auth-box">
|
||||
<!-- 加载中 -->
|
||||
<template v-if="status === 'loading'">
|
||||
<view class="auth-icon">
|
||||
<uni-icons type="spinner-cycle" size="72" color="#EC0208" class="spin-animation" />
|
||||
</view>
|
||||
<view class="auth-title">{{ title }}</view>
|
||||
<view class="auth-desc">{{ desc }}</view>
|
||||
</template>
|
||||
|
||||
<!-- 成功 -->
|
||||
<template v-if="status === 'success'">
|
||||
<view class="auth-icon">
|
||||
<uni-icons type="checkmarkempty" size="72" color="#07c160" />
|
||||
</view>
|
||||
<view class="auth-title">{{ title }}</view>
|
||||
<view class="auth-desc">{{ desc }}</view>
|
||||
</template>
|
||||
|
||||
<!-- 失败 -->
|
||||
<template v-if="status === 'fail'">
|
||||
<view class="auth-icon">
|
||||
<uni-icons type="closeempty" size="72" color="#ee0a24" />
|
||||
</view>
|
||||
<view class="auth-title">{{ title }}</view>
|
||||
<view class="auth-desc">{{ desc }}</view>
|
||||
</template>
|
||||
|
||||
<!-- 复制链接 -->
|
||||
<template v-if="status === 'CopyUrl'">
|
||||
<view class="auth-icon">
|
||||
<uni-icons type="link" size="72" color="#1989fa" />
|
||||
</view>
|
||||
<view class="auth-title">{{ title }}</view>
|
||||
<view class="auth-desc">{{ desc }}</view>
|
||||
<button class="auth-btn auth-btn--primary" v-if="CopyLink" @click="copyText">
|
||||
<uni-icons type="compose" size="16" color="#ffffff" />
|
||||
<text class="btn-text">复制授权地址</text>
|
||||
</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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
|
||||
}
|
||||
},
|
||||
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) {
|
||||
// 场景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('授权链接异常,请重新授权!');
|
||||
}
|
||||
},
|
||||
|
||||
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('获取用户信息失败!');
|
||||
}
|
||||
},
|
||||
|
||||
handleUserError(err) {
|
||||
console.error('获取用户信息失败:', err);
|
||||
const msg = err.raw?.data?.msg || err.raw?.data?.message || err.message || '获取用户信息失败,请检查网络';
|
||||
this.showFail(msg);
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
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';
|
||||
},
|
||||
}
|
||||
}
|
||||
</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-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);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 44rpx;
|
||||
color: #1a1a1a;
|
||||
margin: 24rpx 0 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.auth-desc {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 56rpx;
|
||||
line-height: 1.8;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="category-container">
|
||||
<!-- 头部搜索与导航 (1:1 还原首页样式) -->
|
||||
<view class="header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar-wrap" @click="goSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<scroll-view scroll-y class="main-content" @scrolltolower="loadMore">
|
||||
<!-- 占位符适配固定头部 -->
|
||||
<view class="header-placeholder" :style="{ height: (statusBarHeight + 90) + 'px' }"></view>
|
||||
<view class="header-placeholder" :style="{ height: '90px' }"></view>
|
||||
|
||||
<!-- 子分类金刚区 (支持翻页) -->
|
||||
<view class="sub-grid-wrap" v-if="subCategories.length > 0">
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="detail-page-container">
|
||||
<!-- 沉浸式顶部标题栏 -->
|
||||
<view class="nav-header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="header-content">
|
||||
<view class="back-area" @click="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore">
|
||||
<!-- 占位适配固定头部 -->
|
||||
<view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<view class="header-placeholder" :style="{ height: '44px' }"></view>
|
||||
|
||||
<!-- 排序筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
|
|
@ -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(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(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,27 +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 => ({
|
||||
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) {
|
||||
|
|
@ -287,9 +317,10 @@
|
|||
loadMore() {
|
||||
this.getProducts();
|
||||
},
|
||||
goToDetail(id) {
|
||||
goToDetail(key, id) {
|
||||
// console.log(`/pages/detail/detail?${key}=${id}`)
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/detail?id=${id}`
|
||||
url: `/pages/detail/detail?${key}=${id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -544,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;
|
||||
|
|
@ -557,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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<view class="classify-container">
|
||||
<view class="classify-header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="header-content">
|
||||
<view class="search-bar" @click="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<view class="main-body" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="main-body" :style="{ paddingTop: '44px' }">
|
||||
<!-- 左侧一级菜单 -->
|
||||
<scroll-view scroll-y class="side-bar" :show-scrollbar="false">
|
||||
<view class="side-item" :class="{ 'active': activeIndex === index }"
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
<template>
|
||||
<view class="detail-container">
|
||||
<!-- 顶部渐变导航栏 -->
|
||||
<view class="fixed-header" :style="{ opacity: navOpacity, paddingTop: statusBarHeight + 'px' }">
|
||||
|
||||
<view class="fixed-header" :style="{ opacity: navOpacity }">
|
||||
<view class="header-content">
|
||||
<text class="header-title">商品详情</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮返回按钮 -->
|
||||
<view class="back-btn" :style="{ top: statusBarHeight + 'px', backgroundColor: 'rgba(0,0,0,' + bgOpacity + ')' }" @click="goBack">
|
||||
|
||||
<view class="back-btn" :style="{ backgroundColor: 'rgba(0,0,0,' + bgOpacity + ')' }" @click="goBack">
|
||||
<view class="back-icon-box" :style="{ borderColor: iconColor }"></view>
|
||||
</view>
|
||||
|
||||
|
|
@ -28,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>
|
||||
|
||||
<!-- 热销榜单提示 -->
|
||||
|
|
@ -66,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>
|
||||
|
||||
<!-- 优惠券模块 -->
|
||||
|
|
@ -78,11 +83,11 @@
|
|||
</view>
|
||||
<view class="coupon-time">使用期限: {{ product.couponTime }}</view>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<!-- <view class="coupon-right">
|
||||
<view class="get-btn" @click="goBuy">立即领券</view>
|
||||
</view>
|
||||
<view class="coupon-circle top"></view>
|
||||
<view class="coupon-circle bottom"></view>
|
||||
</view> -->
|
||||
<!-- <view class="coupon-circle top"></view> -->
|
||||
<!-- <view class="coupon-circle bottom"></view> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -158,7 +163,7 @@
|
|||
</view>
|
||||
<view class="list">
|
||||
<view class="list-container">
|
||||
<view class="g-shop-list" v-for="(item, idx) in similarProducts" :key="idx" @click="goToDetail(item.itemid)">
|
||||
<view class="g-shop-list" v-for="(item, idx) in similarProducts" :key="idx" @click="goToDetail(item.id)">
|
||||
<view class="shop-img">
|
||||
<image :src="item.itempic" mode="aspectFill" lazy-load></image>
|
||||
</view>
|
||||
|
|
@ -178,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>
|
||||
|
|
@ -225,7 +236,7 @@
|
|||
</view>
|
||||
<view class="bar-right">
|
||||
<view class="copy-btn" @click="copyTaoWord">复制口令购买</view>
|
||||
<view class="buy-btn" @click="goBuy">领券购买</view>
|
||||
<!-- <view class="buy-btn" @click="goBuy">领券购买</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -272,17 +283,24 @@
|
|||
},
|
||||
onLoad(options) {
|
||||
this.product.id = options.id || '55493973';
|
||||
this.keywordid = options.keywordid || '';
|
||||
const sysInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = sysInfo.statusBarHeight || 44;
|
||||
this.getDetailData();
|
||||
},
|
||||
methods: {
|
||||
getDetailData() {
|
||||
const useSuperSearch = this.keywordid;
|
||||
uni.request({
|
||||
url: `https://api.cmspro.haodanku.com/detail/itemInfo?id=${this.product.id}&cid=YsWZ21tx`,
|
||||
url: useSuperSearch
|
||||
? `https://api.cmspro.haodanku.com/superSearch/getList?keyword=${this.keywordid}&cid=YsWZ21tx`
|
||||
: `https://api.cmspro.haodanku.com/detail/itemInfo?id=${this.product.id}&cid=YsWZ21tx`,
|
||||
success: (res) => {
|
||||
if (res.data && res.data.code === 200) {
|
||||
const d = res.data.data;
|
||||
const d = useSuperSearch
|
||||
? (Array.isArray(res.data.data) && res.data.data.length > 0 ? res.data.data[0] : null)
|
||||
: res.data.data;
|
||||
if (!d) return;
|
||||
console.log('详情数据获取成功:', d);
|
||||
|
||||
// 格式化活动时间
|
||||
|
|
@ -379,12 +397,19 @@
|
|||
this.getSimilarProducts(d.itemid, d.son_category);
|
||||
|
||||
this.isLoaded = true;
|
||||
// 预加载淘口令和领券链接,避免 iOS 异步剪贴板限制
|
||||
this.fetchTaoWord();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchTaoWord(callback) {
|
||||
if (!this.product.taobaoId) return;
|
||||
// 已获取过则直接回调,避免重复请求,同时保证 iOS 剪贴板在同步路径中
|
||||
if (this.product.taoCode || this.product.couponUrl) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '加载中...', mask: true });
|
||||
uni.request({
|
||||
url: 'https://v2.api.haodanku.com/ratesurl',
|
||||
|
|
@ -398,7 +423,7 @@
|
|||
get_taoword: 1,
|
||||
pid: 'mm_284380119_1881450385_111415850448',
|
||||
tb_name: 'michuan2018',
|
||||
relation_id: '2864040832',
|
||||
relation_id: this.$store.getters.relationId || '',
|
||||
apikey: '5417B681C5EA'
|
||||
},
|
||||
success: (res) => {
|
||||
|
|
@ -423,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
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -435,6 +463,7 @@
|
|||
return sale;
|
||||
},
|
||||
goToDetail(id) {
|
||||
// console.log(id)
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/detail?id=${id}`
|
||||
});
|
||||
|
|
@ -466,17 +495,23 @@
|
|||
uni.showToast({ title: '暂无领券链接', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// #ifdef H5
|
||||
window.location.href = url;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
uni.showToast({ title: '链接已复制,请到浏览器打开', icon: 'none' });
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
// // #ifdef H5
|
||||
// window.location.href = url;
|
||||
// // #endif
|
||||
// // #ifndef H5
|
||||
// uni.setClipboardData({
|
||||
// data: url,
|
||||
// success: () => {
|
||||
// uni.showToast({ title: '链接已复制,请到浏览器打开', icon: 'none' });
|
||||
// }
|
||||
// });
|
||||
// // #endif
|
||||
});
|
||||
},
|
||||
handleScroll(e) {
|
||||
|
|
@ -491,7 +526,7 @@
|
|||
this.iconColor = ratio > 0.5 ? '#333333' : '#ffffff';
|
||||
},
|
||||
goHome() {
|
||||
uni.switchTab({
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
},
|
||||
|
|
@ -880,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;
|
||||
|
|
@ -1074,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;
|
||||
|
|
@ -1610,7 +1670,8 @@
|
|||
|
||||
.copy-btn {
|
||||
flex: 1;
|
||||
background-color: #ffbcba;
|
||||
/* background-color: #ffbcba; */
|
||||
background: linear-gradient(to right, #ff715a, #ff416c);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<view class="container">
|
||||
<!-- 头部搜索与导航 -->
|
||||
<!-- :style="{ paddingTop: statusBarHeight + 'px' }" -->
|
||||
<view class="header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<view class="search-section">
|
||||
<view class="search-bar-wrap" @click="goSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="main-content" :show-scrollbar="false" :scroll-into-view="scrollTarget" scroll-with-animation>
|
||||
<!-- :style="{ paddingTop: headerHeight + 'px' }" -->
|
||||
<view class="main-content" :style="{ paddingTop: '120px' }">
|
||||
<!-- 轮播图区域 -->
|
||||
<view class="banner-box" id="top-section">
|
||||
<swiper class="swiper" circular autoplay indicator-dots indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#ffffff">
|
||||
|
|
@ -240,13 +240,19 @@
|
|||
</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>
|
||||
</view>
|
||||
|
||||
<view class="loading-more">-- 到底啦 --</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 通用底部 Tab 栏 -->
|
||||
<bottom-nav :activeTab="0"></bottom-nav>
|
||||
|
|
@ -255,6 +261,7 @@
|
|||
|
||||
<script>
|
||||
import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
|
||||
import http from '@/request/request.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -266,7 +273,6 @@
|
|||
activeBottomTab: 0,
|
||||
currentTab: 0,
|
||||
navList: [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }],
|
||||
scrollTarget: '',
|
||||
banners: [
|
||||
'https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&q=80',
|
||||
'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=800&q=80',
|
||||
|
|
@ -313,6 +319,17 @@
|
|||
this.getWorthBuyLists();
|
||||
this.getBrandSaleList();
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.getIndexData();
|
||||
this.getCategoryList();
|
||||
this.getNoticeList();
|
||||
this.getGoodsList();
|
||||
this.getWorthBuyLists();
|
||||
this.getBrandSaleList();
|
||||
setTimeout(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
}, 600);
|
||||
},
|
||||
computed: {
|
||||
gridMenuPages() {
|
||||
const pages = [];
|
||||
|
|
@ -344,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;
|
||||
}
|
||||
|
|
@ -360,46 +374,40 @@
|
|||
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) {
|
||||
console.log('正在跳转到详情页,商品ID:', id);
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/detail?id=${id}`
|
||||
// url: `/pages/activity/activity`
|
||||
});
|
||||
},
|
||||
|
||||
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) {
|
||||
|
|
@ -415,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,
|
||||
|
|
@ -443,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 || '大牌特卖',
|
||||
|
|
@ -472,7 +476,8 @@
|
|||
}))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取品牌特卖失败:', err.message);
|
||||
});
|
||||
},
|
||||
switchTab(index) {
|
||||
|
|
@ -489,13 +494,9 @@
|
|||
}
|
||||
|
||||
this.currentTab = index;
|
||||
this.scrollTarget = ''; // 重置目标,确保下次点击仍能触发滚动
|
||||
this.$nextTick(() => {
|
||||
if (tabName === '推荐') {
|
||||
this.scrollTarget = 'goods-list-section';
|
||||
} else if (tabName === '首页') {
|
||||
this.scrollTarget = 'top-section';
|
||||
}
|
||||
uni.pageScrollTo({
|
||||
selector: tabName === '推荐' ? '#goods-list-section' : '#top-section',
|
||||
duration: 300
|
||||
});
|
||||
},
|
||||
goToChoiceness() {
|
||||
|
|
@ -515,14 +516,15 @@
|
|||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
padding-bottom: 10rpx;
|
||||
z-index: 100;
|
||||
|
|
@ -620,13 +622,12 @@
|
|||
|
||||
/* 主体滚动区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 轮播图 */
|
||||
.banner-box {
|
||||
padding: 20rpx 30rpx 10rpx;
|
||||
padding: 0 30rpx 10rpx;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
|
|
@ -1625,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;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="rank-container">
|
||||
<!-- 沉浸式渐变头部 -->
|
||||
<view class="rank-header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<!-- 榜单类型切换 -->
|
||||
<view class="rank-main-tabs">
|
||||
<view class="main-tab" :class="{ 'active': rankType === 1 }" @click="switchRankType(1)">
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<scroll-view scroll-y class="rank-content" @scrolltolower="loadMore">
|
||||
<!-- 顶部背景延伸区 (仅作为背景块,不包裹卡片) -->
|
||||
<view class="rank-bg-header" :style="{ height: (statusBarHeight + 110) + 'px' }"></view>
|
||||
<view class="rank-bg-header" :style="{ height: '110px' }"></view>
|
||||
|
||||
<!-- Top 1-3 领奖台布局 (参考 shop-sy2 样式) -->
|
||||
<view class="top-three-wrap" v-if="goodsList.length >= 3">
|
||||
|
|
@ -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) {
|
||||
|
|
@ -221,7 +220,7 @@
|
|||
let itemSale2Str = itemSale2 >= 10000 ? (itemSale2 / 10000).toFixed(1) + '万' : itemSale2;
|
||||
|
||||
return {
|
||||
id: item.itemid,
|
||||
id: item.id,
|
||||
image: item.itempic,
|
||||
title: title,
|
||||
finalPrice: item.itemendprice,
|
||||
|
|
@ -235,10 +234,9 @@
|
|||
});
|
||||
this.goodsList = [...this.goodsList, ...list];
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
loadMore() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="save-money-container">
|
||||
<!-- 沉浸式状态栏与高定自定义顶部导航条 -->
|
||||
<view class="custom-nav-bar">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="nav-header">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<text class="back-arrow">〈</text>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
</view>
|
||||
|
||||
<!-- 占位符防止图片被顶部悬浮遮挡 -->
|
||||
<view class="nav-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<view class="nav-placeholder" :style="{ height: '44px' }"></view>
|
||||
|
||||
<!-- 核心内容区:无缝连接的两张全景自适应介绍照片 -->
|
||||
<view class="photo-flow">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="search-container">
|
||||
<!-- 头部搜索栏 -->
|
||||
<view class="search-header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="header-main">
|
||||
<view class="back-btn" @click="goBack" hover-class="back-btn-hover">
|
||||
<text class="back-arrow">‹</text>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="search-body" :style="{ paddingTop: (statusBarHeight + 96) + 'px' }"
|
||||
<scroll-view scroll-y class="search-body" :style="{ paddingTop: '96px' }"
|
||||
@scrolltolower="onScrollBottom">
|
||||
<!-- 搜索结果列表 -->
|
||||
<block v-if="isSearching">
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="choiceness-container">
|
||||
<!-- 自定义沉浸式头部导航 -->
|
||||
<view class="custom-header">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-left back-area" @click="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
</view>
|
||||
|
||||
<!-- 占位符确保内容不被顶部悬浮覆盖 -->
|
||||
<view class="header-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||||
<view class="header-placeholder" :style="{ height: '44px' }"></view>
|
||||
|
||||
<!-- 顶部深紫渐变横向品牌滚动专区 (支持右滑到底无限加载) -->
|
||||
<view class="top-scroll-brands-section">
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<view class="brand-detail-container">
|
||||
<!-- 自定义沉浸式头部导航 -->
|
||||
<view class="custom-header" :class="{ 'header-scrolled': scrollY > 50 }">
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-left back-area" @click="goBack">
|
||||
<text class="back-icon">〈</text>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<view class="biography-overlay"></view>
|
||||
|
||||
<!-- 品牌身份核心图腾层 -->
|
||||
<view class="biography-core" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="biography-core" :style="{ paddingTop: '44px' }">
|
||||
<view class="logo-wrapper">
|
||||
<image class="brand-logo-img" :src="brandInfo.brand_logo || brandInfo.inside_logo" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
|
@ -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 };
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
/**
|
||||
* ============================================================
|
||||
* URL 参数解析工具 - url-query.js
|
||||
* 支持 uni-app 全平台(H5、微信小程序、支付宝小程序、
|
||||
* 百度小程序、抖音小程序、App、快应用)
|
||||
* ============================================================
|
||||
*
|
||||
* 【使用方式】
|
||||
* import UrlQuery from '@/scratch/url-query.js';
|
||||
*
|
||||
* // 获取单个参数(自动类型转换)
|
||||
* const id = UrlQuery.get('id'); // "123" → 123 (number)
|
||||
* const name = UrlQuery.get('name'); // "张三" (string)
|
||||
* const flag = UrlQuery.get('flag'); // "true" → true (boolean)
|
||||
* const rid = UrlQuery.get('rid', '-1'); // 不存在返回默认值 '-1'
|
||||
*
|
||||
* // 获取原始字符串(不自动类型转换)
|
||||
* const raw = UrlQuery.get('id', '', false); // "123" (string)
|
||||
*
|
||||
* // 获取所有参数
|
||||
* const params = UrlQuery.getAll(); // { id: 123, name: "张三" }
|
||||
*
|
||||
* // 判断参数是否存在
|
||||
* const hasId = UrlQuery.has('id'); // true / false
|
||||
*
|
||||
* // 解析指定 URL 字符串
|
||||
* const parsed = UrlQuery.parseUrl('https://example.com?id=1&name=a');
|
||||
*
|
||||
* // 解析查询字符串
|
||||
* const obj = UrlQuery.parse('?a=1&b=2&b=3'); // { a: "1", b: ["2", "3"] }
|
||||
*
|
||||
* 【特性】
|
||||
* 1. 多平台自适应(H5 通过 location 获取,小程序/App 通过页面栈获取)
|
||||
* 2. 自动类型转换(数字、布尔值、null)
|
||||
* 3. 安全解码(异常编码不会报错)
|
||||
* 4. 支持数组参数(同名参数自动转为数组)
|
||||
* 5. 支持 history 路由和 hash 路由模式
|
||||
* 6. 容错处理(空值、undefined、异常 URL 等)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
const UrlQuery = {
|
||||
/**
|
||||
* 解析 URL 查询字符串为对象
|
||||
* @param {string} queryString - 查询字符串,如 "?a=1&b=2" 或 "a=1&b=2"
|
||||
* @returns {Object} 解析后的参数对象,同名参数自动转为数组
|
||||
*/
|
||||
parse(queryString) {
|
||||
const result = {};
|
||||
|
||||
if (typeof queryString !== 'string') {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 统一去掉开头的 ? 和 #,并去除首尾空白
|
||||
queryString = queryString.replace(/^[?#]/, '').trim();
|
||||
if (!queryString) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const pairs = queryString.split('&');
|
||||
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const pair = pairs[i];
|
||||
if (!pair) continue;
|
||||
|
||||
const eqIndex = pair.indexOf('=');
|
||||
let key, value;
|
||||
|
||||
if (eqIndex === -1) {
|
||||
// 无等号的情况,如 "?foo&bar"
|
||||
key = pair;
|
||||
value = '';
|
||||
} else {
|
||||
key = pair.substring(0, eqIndex);
|
||||
value = pair.substring(eqIndex + 1);
|
||||
}
|
||||
|
||||
// 安全解码(异常编码不抛错)
|
||||
try {
|
||||
key = decodeURIComponent(key);
|
||||
} catch (e) {
|
||||
/* 保留原始值 */
|
||||
}
|
||||
try {
|
||||
value = decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
/* 保留原始值 */
|
||||
}
|
||||
|
||||
// 处理数组参数(如 id=1&id=2 → { id: ['1', '2'] })
|
||||
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||
if (!Array.isArray(result[key])) {
|
||||
result[key] = [result[key]];
|
||||
}
|
||||
result[key].push(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前页面的 URL 参数对象
|
||||
* 多平台自适应:H5 从 window.location 获取,其他平台从页面栈获取
|
||||
* @returns {Object} 当前页面所有参数
|
||||
*/
|
||||
getCurrent() {
|
||||
let params = {};
|
||||
|
||||
// #ifdef H5
|
||||
// H5 环境下优先从 window.location 解析(兼容 history 和 hash 路由)
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
const searchParams = this.parse(window.location.search);
|
||||
const hashParts = window.location.hash.split('?');
|
||||
const hashParams = hashParts.length > 1 ? this.parse(hashParts[1]) : {};
|
||||
params = { ...searchParams, ...hashParams };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[UrlQuery] H5 解析 location 失败:', e);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 尝试从页面栈获取(uni-app 所有平台通用)
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages && pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage && currentPage.options && typeof currentPage.options === 'object') {
|
||||
// H5 下合并 location 与 options,其他平台以 options 为准
|
||||
// #ifdef H5
|
||||
params = { ...params, ...currentPage.options };
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
params = { ...currentPage.options };
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// getCurrentPages 在部分环境下可能不可用,静默处理
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
/**
|
||||
* 解析指定 URL 字符串中的参数
|
||||
* @param {string} url - 完整 URL,如 "https://example.com/path?id=1&name=a#hash?b=2"
|
||||
* @returns {Object} 解析后的参数对象(search 参数优先,hash 参数补充)
|
||||
*/
|
||||
parseUrl(url) {
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
url = url.trim();
|
||||
|
||||
// 优先使用浏览器 URL API(H5 / 现代环境)
|
||||
try {
|
||||
if (typeof URL !== 'undefined') {
|
||||
const urlObj = new URL(url);
|
||||
return this.parse(urlObj.search);
|
||||
}
|
||||
} catch (e) {
|
||||
// URL 构造函数不支持或解析失败,降级到手动提取
|
||||
}
|
||||
|
||||
// 降级处理:手动提取 search 和 hash 中的参数
|
||||
let search = '';
|
||||
let hashQuery = '';
|
||||
|
||||
const searchIndex = url.indexOf('?');
|
||||
const hashIndex = url.indexOf('#');
|
||||
|
||||
if (searchIndex !== -1) {
|
||||
const endIndex = (hashIndex !== -1 && hashIndex > searchIndex) ? hashIndex : url.length;
|
||||
search = url.substring(searchIndex, endIndex);
|
||||
}
|
||||
|
||||
if (hashIndex !== -1) {
|
||||
const hashPart = url.substring(hashIndex + 1);
|
||||
const hashQIndex = hashPart.indexOf('?');
|
||||
if (hashQIndex !== -1) {
|
||||
hashQuery = hashPart.substring(hashQIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = this.parse(search);
|
||||
const hashParams = this.parse(hashQuery);
|
||||
|
||||
return { ...searchParams, ...hashParams };
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取单个参数值
|
||||
* @param {string} key - 参数名(必填)
|
||||
* @param {*} defaultValue - 参数不存在时返回的默认值(默认空字符串)
|
||||
* @param {boolean} autoType - 是否自动类型转换(默认 true)
|
||||
* true : "123"→123, "true"→true, "false"→false, "null"→null
|
||||
* false : 始终返回原始字符串
|
||||
* @returns {*} 参数值
|
||||
*/
|
||||
get(key, defaultValue = '', autoType = true) {
|
||||
if (typeof key !== 'string' || !key) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const params = this.getCurrent();
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(params, key)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
let value = params[key];
|
||||
|
||||
// 值为 undefined / null 时返回默认值
|
||||
if (value === undefined || value === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// 不自动类型转换时直接返回字符串形式
|
||||
if (!autoType) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// 已经是非字符串类型(如数字、布尔)直接返回
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 空字符串处理
|
||||
if (value === '') {
|
||||
return defaultValue !== '' ? defaultValue : '';
|
||||
}
|
||||
|
||||
// 布尔值转换
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
|
||||
// null / undefined 字符串转换
|
||||
if (value === 'null') return null;
|
||||
if (value === 'undefined') return undefined;
|
||||
|
||||
// 整数转换(严格匹配,避免 "0123" 被转成 123 后丢失前导零)
|
||||
if (/^-?(0|[1-9]\d*)$/.test(value)) {
|
||||
const intVal = parseInt(value, 10);
|
||||
if (!isNaN(intVal) && String(intVal) === value) {
|
||||
return intVal;
|
||||
}
|
||||
}
|
||||
|
||||
// 浮点数转换
|
||||
if (/^-?(0|[1-9]\d*)\.\d+$/.test(value)) {
|
||||
const floatVal = parseFloat(value);
|
||||
if (!isNaN(floatVal) && String(floatVal) === value) {
|
||||
return floatVal;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有参数(原始字符串形式,不做类型转换)
|
||||
* @returns {Object} 所有参数的字符串键值对
|
||||
*/
|
||||
getAll() {
|
||||
return this.getCurrent();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有参数并进行自动类型转换
|
||||
* @returns {Object} 所有参数(数字、布尔值已转换)
|
||||
*/
|
||||
getAllTyped() {
|
||||
const params = this.getCurrent();
|
||||
const result = {};
|
||||
for (const key in params) {
|
||||
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
||||
result[key] = this.get(key, '', true);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断参数是否存在于当前 URL 中
|
||||
* @param {string} key - 参数名
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(key) {
|
||||
if (typeof key !== 'string' || !key) {
|
||||
return false;
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(this.getCurrent(), key);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取数组类型的参数值
|
||||
* 若参数为单个值则包装为数组返回,不存在返回默认值
|
||||
* @param {string} key - 参数名
|
||||
* @param {Array} defaultValue - 默认值(默认空数组)
|
||||
* @returns {Array} 数组形式的参数值
|
||||
*/
|
||||
getArray(key, defaultValue = []) {
|
||||
if (typeof key !== 'string' || !key) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const params = this.getCurrent();
|
||||
if (!Object.prototype.hasOwnProperty.call(params, key)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const value = params[key];
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return [value];
|
||||
}
|
||||
};
|
||||
|
||||
// 兼容 CommonJS / ES Module / 全局变量
|
||||
// #ifdef VUE3
|
||||
export default UrlQuery;
|
||||
// #endif
|
||||
|
||||
// #ifndef VUE3
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = UrlQuery;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define(function () { return UrlQuery; });
|
||||
} else {
|
||||
try {
|
||||
window.UrlQuery = UrlQuery;
|
||||
} catch (e) {
|
||||
// 非浏览器环境静默处理
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { createStore } from 'vuex'
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
currentUid: '',
|
||||
relationId: '',
|
||||
pid: '',
|
||||
isThirdParty: false
|
||||
},
|
||||
getters: {
|
||||
currentUid: state => state.currentUid,
|
||||
relationId: state => state.relationId,
|
||||
pid: state => state.pid,
|
||||
isThirdParty: state => state.isThirdParty
|
||||
},
|
||||
mutations: {
|
||||
SET_CURRENT_UID(state, uid) {
|
||||
state.currentUid = uid
|
||||
},
|
||||
CLEAR_CURRENT_UID(state) {
|
||||
state.currentUid = ''
|
||||
},
|
||||
SET_RELATION_ID(state, rid) {
|
||||
state.relationId = rid
|
||||
},
|
||||
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: {
|
||||
setCurrentUid({ commit }, uid) {
|
||||
commit('SET_CURRENT_UID', uid)
|
||||
},
|
||||
clearCurrentUid({ commit }) {
|
||||
commit('CLEAR_CURRENT_UID')
|
||||
},
|
||||
setRelationId({ commit }, rid) {
|
||||
commit('SET_RELATION_ID', rid)
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
|
|
@ -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,271 @@
|
|||
/**
|
||||
* ============================================================
|
||||
* 环境检测器 - uni-app 适配版
|
||||
* 支持:H5、微信小程序、支付宝小程序、百度小程序、抖音小程序、App
|
||||
* ============================================================
|
||||
*
|
||||
* 原始版本基于 navigator.userAgent(H5 专用)
|
||||
* 本版本通过 uni-app 条件编译适配多端
|
||||
*
|
||||
* 【用法】
|
||||
* import { getEnvironment, detect, isWechat, isBrowser, ... } from '@/utils/env.js';
|
||||
*
|
||||
* const env = getEnvironment();
|
||||
* if (env.isBrowser) { ... }
|
||||
* if (env.isWechatWebview) { ... }
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/* ========== H5 平台:原始 UA 检测 ========== */
|
||||
// #ifdef H5
|
||||
const UA = (navigator.userAgent || '').toLowerCase();
|
||||
|
||||
function _isWechatMiniProgram() {
|
||||
if (window.__wxjs_environment === 'miniprogram') return true;
|
||||
if (/miniprogram/.test(UA)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isWechatH5() {
|
||||
if (!/micromessenger/.test(UA)) return false;
|
||||
if (_isWechatMiniProgram()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function _isWechat() {
|
||||
return /micromessenger/.test(UA);
|
||||
}
|
||||
|
||||
function _isAppWebview() {
|
||||
const patterns = [
|
||||
/aliapp\(tb/i,
|
||||
/aliapp\(tm/i,
|
||||
/jdapp/i,
|
||||
/aweme/i,
|
||||
/ksnc/i,
|
||||
/weibo/i,
|
||||
/dingtalk/i,
|
||||
/alipayclient/i,
|
||||
/baiduboxapp/i,
|
||||
/qq\//i,
|
||||
];
|
||||
return patterns.some(function (re) { return re.test(UA); });
|
||||
}
|
||||
|
||||
function _isAppNative() {
|
||||
if (typeof window.TBJS !== 'undefined') return true;
|
||||
if (typeof window.AlipayJSBridge !== 'undefined') return true;
|
||||
if (typeof window.dd !== 'undefined') return true;
|
||||
if (typeof window.toutiao !== 'undefined') return true;
|
||||
if (typeof window.JDBridge !== 'undefined') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isBrowser() {
|
||||
if (_isWechat()) return false;
|
||||
if (_isAppWebview()) return false;
|
||||
if (_isAppNative()) return false;
|
||||
return /safari|chrome|crios|firefox|fxios|edge|edg|opr/.test(UA);
|
||||
}
|
||||
|
||||
function _isMobile() {
|
||||
return /iphone|ipad|ipod|android/.test(UA);
|
||||
}
|
||||
|
||||
function _getAppName() {
|
||||
var map = [
|
||||
{ name: 'taobao', regex: /aliapp\(tb/i },
|
||||
{ name: 'tmall', regex: /aliapp\(tm/i },
|
||||
{ name: 'jd', regex: /jdapp/i },
|
||||
{ name: 'douyin', regex: /aweme/i },
|
||||
{ name: 'kuaishou', regex: /ksnc/i },
|
||||
{ name: 'weibo', regex: /weibo/i },
|
||||
{ name: 'dingtalk', regex: /dingtalk/i },
|
||||
{ name: 'alipay', regex: /alipayclient/i },
|
||||
{ name: 'baidu', regex: /baiduboxapp/i },
|
||||
{ name: 'qq', regex: /qq\//i },
|
||||
];
|
||||
for (var i = 0; i < map.length; i++) {
|
||||
if (map[i].regex.test(UA)) return map[i].name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _detect() {
|
||||
if (_isWechatMiniProgram()) return 'wechat-miniprogram';
|
||||
if (_isWechatH5()) return 'wechat-h5';
|
||||
if (_isAppNative()) return 'app-native';
|
||||
if (_isAppWebview()) return 'app-webview';
|
||||
if (_isBrowser()) return _isMobile() ? 'mobile-browser' : 'pc-browser';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function _getEnvironment() {
|
||||
var wechat = _isWechat();
|
||||
var wechatMini = _isWechatMiniProgram();
|
||||
var wechatH5Env = _isWechatH5();
|
||||
var appWebview = _isAppWebview();
|
||||
var appNative = _isAppNative();
|
||||
var browser = _isBrowser();
|
||||
|
||||
return {
|
||||
isApp: appNative,
|
||||
isWechat: wechat,
|
||||
isWechatH5: wechatH5Env,
|
||||
isBrowser: browser,
|
||||
isWechatWebview: wechatH5Env || wechatMini,
|
||||
isAppWebview: appWebview,
|
||||
isWechatMiniProgram: wechatMini,
|
||||
isAppNative: appNative,
|
||||
appName: _getAppName(),
|
||||
isMobile: _isMobile(),
|
||||
env: _detect()
|
||||
};
|
||||
}
|
||||
// #endif
|
||||
|
||||
/* ========== 微信小程序 ========== */
|
||||
// #ifdef MP-WEIXIN
|
||||
function _getEnvironment() {
|
||||
return {
|
||||
isApp: false,
|
||||
isWechat: true,
|
||||
isWechatH5: false,
|
||||
isBrowser: false,
|
||||
isWechatWebview: true,
|
||||
isAppWebview: false,
|
||||
isWechatMiniProgram: true,
|
||||
isAppNative: false,
|
||||
appName: null,
|
||||
isMobile: true,
|
||||
env: 'wechat-miniprogram'
|
||||
};
|
||||
}
|
||||
function _detect() { return 'wechat-miniprogram'; }
|
||||
function _isWechatMiniProgram() { return true; }
|
||||
function _isWechatH5() { return false; }
|
||||
function _isWechat() { return true; }
|
||||
function _isWechatWebview() { return true; }
|
||||
function _isAppWebview() { return false; }
|
||||
function _isAppNative() { return false; }
|
||||
function _isBrowser() { return false; }
|
||||
function _isMobile() { return true; }
|
||||
function _getAppName() { return null; }
|
||||
// #endif
|
||||
|
||||
/* ========== App 平台 ========== */
|
||||
// #ifdef APP-PLUS
|
||||
function _getEnvironment() {
|
||||
return {
|
||||
isApp: false,
|
||||
isWechat: false,
|
||||
isWechatH5: false,
|
||||
isBrowser: false,
|
||||
isWechatWebview: false,
|
||||
isAppWebview: true,
|
||||
isWechatMiniProgram: false,
|
||||
isAppNative: false,
|
||||
appName: null,
|
||||
isMobile: true,
|
||||
env: 'app-webview'
|
||||
};
|
||||
}
|
||||
function _detect() { return 'app-webview'; }
|
||||
function _isWechatMiniProgram() { return false; }
|
||||
function _isWechatH5() { return false; }
|
||||
function _isWechat() { return false; }
|
||||
function _isWechatWebview() { return false; }
|
||||
function _isAppWebview() { return true; }
|
||||
function _isAppNative() { return false; }
|
||||
function _isBrowser() { return false; }
|
||||
function _isMobile() { return true; }
|
||||
function _getAppName() { return null; }
|
||||
// #endif
|
||||
|
||||
/* ========== 其他小程序平台(支付宝、百度、抖音等) ========== */
|
||||
// #ifndef H5 || MP-WEIXIN || APP-PLUS
|
||||
function _getEnvironment() {
|
||||
return {
|
||||
isApp: false,
|
||||
isWechat: false,
|
||||
isWechatH5: false,
|
||||
isBrowser: false,
|
||||
isWechatWebview: false,
|
||||
isAppWebview: false,
|
||||
isWechatMiniProgram: false,
|
||||
isAppNative: false,
|
||||
appName: null,
|
||||
isMobile: true,
|
||||
env: 'unknown'
|
||||
};
|
||||
}
|
||||
function _detect() { return 'unknown'; }
|
||||
function _isWechatMiniProgram() { return false; }
|
||||
function _isWechatH5() { return false; }
|
||||
function _isWechat() { return false; }
|
||||
function _isWechatWebview() { return false; }
|
||||
function _isAppWebview() { return false; }
|
||||
function _isAppNative() { return false; }
|
||||
function _isBrowser() { return false; }
|
||||
function _isMobile() { return true; }
|
||||
function _getAppName() { return null; }
|
||||
// #endif
|
||||
|
||||
/* ========== 统一暴露接口 ========== */
|
||||
export function getEnvironment() {
|
||||
return _getEnvironment();
|
||||
}
|
||||
|
||||
export function detect() {
|
||||
return _detect();
|
||||
}
|
||||
|
||||
export function isWechatMiniProgram() {
|
||||
return _isWechatMiniProgram();
|
||||
}
|
||||
|
||||
export function isWechatH5() {
|
||||
return _isWechatH5();
|
||||
}
|
||||
|
||||
export function isWechat() {
|
||||
return _isWechat();
|
||||
}
|
||||
|
||||
export function isWechatWebview() {
|
||||
return _isWechatWebview();
|
||||
}
|
||||
|
||||
export function isAppWebview() {
|
||||
return _isAppWebview();
|
||||
}
|
||||
|
||||
export function isAppNative() {
|
||||
return _isAppNative();
|
||||
}
|
||||
|
||||
export function isBrowser() {
|
||||
return _isBrowser();
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return _isMobile();
|
||||
}
|
||||
|
||||
export function getAppName() {
|
||||
return _getAppName();
|
||||
}
|
||||
|
||||
export default {
|
||||
getEnvironment,
|
||||
detect,
|
||||
isWechatMiniProgram,
|
||||
isWechatH5,
|
||||
isWechat,
|
||||
isWechatWebview,
|
||||
isAppWebview,
|
||||
isAppNative,
|
||||
isBrowser,
|
||||
isMobile,
|
||||
getAppName
|
||||
};
|
||||
|
|
@ -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