Compare commits

..

2 Commits

27 changed files with 3323 additions and 372 deletions

56
App.vue
View File

@ -1,13 +1,59 @@
<script> <script>
import UrlQuery from '@/scratch/url-query.js';
export default { export default {
onLaunch: function() { globalData: {
console.log('App Launch') /**
* urlParams - 当前页面 URL 参数集合
*
* 来源
* 1. H5 环境下从 window.locationsearch + hash解析
* 2. 小程序/App 环境下从页面栈 currentPage.options 获取
* 3. 分享/扫码进入时从 App.onLaunch/onShow options.query 合并
*
* 目前业务中实际使用的参数
* - token {string} 用户登录凭证
* - code {string} 淘宝后返回授权码
* - state {string} 状态值格式通常为 "uidxxx"用于提取用户 ID淘宝授权返回
* - exuid {string} 推广人/邀请人用户 ID三方进入用户标识
* - page_uri {string} 授权成功后需要跳转的目标页面路径必须授权过的用户该参数才有效
*
* UrlQuery 支持获取任意 URL 参数以上仅为业务代码中显式消费的字段
*/
urlParams: {}
}, },
onShow: function() { onLaunch: function(options) {
console.log('App Show') 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() { onHide: function() {
console.log('App Hide') console.log('App Hide');
} }
} }
</script> </script>

View File

@ -8,6 +8,8 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
props: { props: {
activeTab: { activeTab: {
@ -25,17 +27,14 @@
}, },
methods: { methods: {
getBottomBar() { getBottomBar() {
uni.request({ http.get('https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx', if (res.data && res.data.bottom_bar) {
success: (res) => {
if (res.data && res.data.code === 200 && res.data.data.bottom_bar) {
// //
const allowedTitles = ['首页', '榜单', '分类']; const allowedTitles = ['首页', '榜单', '分类'];
this.bottomBarList = res.data.data.bottom_bar this.bottomBarList = res.data.bottom_bar
.filter(item => allowedTitles.includes(item.title)) .filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort); .sort((a, b) => b.sort - a.sort);
} }
}
}); });
}, },
handleTabClick(tab, index) { handleTabClick(tab, index) {

View File

@ -1,9 +1,10 @@
import App from './App' import App from './App'
import { estimateCoupon } from './utils/index.js'
// #ifndef VUE3 // #ifndef VUE3
import Vue from 'vue' import Vue from 'vue'
import './uni.promisify.adaptor' import './uni.promisify.adaptor'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.prototype.$estimateCoupon = estimateCoupon;
App.mpType = 'app' App.mpType = 'app'
const app = new Vue({ const app = new Vue({
...App ...App
@ -13,8 +14,11 @@ app.$mount()
// #ifdef VUE3 // #ifdef VUE3
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
import store from './store'
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(store)
app.config.globalProperties.$estimateCoupon = estimateCoupon;
return { return {
app app
} }

View File

@ -1,6 +1,6 @@
{ {
"name" : "baimacms", "name" : "baimacms",
"appid" : "__UNI__404B6E1", "appid" : "__UNI__924A228",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
@ -68,5 +68,11 @@
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false
}, },
"vueVersion" : "3" "vueVersion" : "3",
"h5" : {
"router" : {
"mode" : "history",
"base" : "/affiliate-activity/"
}
}
} }

331
package-lock.json generated Normal file
View File

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

21
package.json Normal file
View File

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

View File

@ -1,63 +1,94 @@
{ {
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": [ "pages": [
{ {
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
} }
}, },
{ {
"path": "pages/detail/detail", "path": "pages/detail/detail",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "商品详情"
} }
}, },
{ {
"path": "pages/category/category", "path": "pages/category/category",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "商品分类"
} }
}, },
{ {
"path": "pages/category/category_detail", "path": "pages/category/category_detail",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "商品分类详情"
} }
}, },
{ {
"path": "pages/rank/rank", "path": "pages/rank/rank",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "商品排名"
} }
}, },
{ {
"path": "pages/classify/classify", "path": "pages/classify/classify",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "商品分类列表"
} }
}, },
{ {
"path": "pages/search/search", "path": "pages/search/search",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "搜索"
} }
}, },
{ {
"path": "pages/special-sale/choicen", "path": "pages/special-sale/choicen",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "特殊销售"
} }
}, },
{ {
"path": "pages/special-sale/details", "path": "pages/special-sale/details",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "特殊销售详情"
} }
}, },
{ {
"path": "pages/save-money/save-money", "path": "pages/save-money/save-money",
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom",
"navigationBarTitleText": "省钱券"
}
},
{
"path": "pages/auth/auth",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "授权"
}
},
{
"path": "pages/activity/activity",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh": true
} }
} }
], ],

634
pages/activity/activity.vue Normal file
View File

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

379
pages/auth/auth.vue Normal file
View File

@ -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) {
// 3uid
this.$store.dispatch('setIsThirdParty', true);
http.get(`${baseUrl}/api/taobao/third/relation_id`, {
uid: this.exuid
}).then(this.handleUserInfo).catch(this.handleUserError);
}else{
this.showFail('授权链接异常,请重新授权!');
}
},
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>

View File

@ -2,7 +2,7 @@
<view class="category-container"> <view class="category-container">
<!-- 头部搜索与导航 (1:1 还原首页样式) --> <!-- 头部搜索与导航 (1:1 还原首页样式) -->
<view class="header"> <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-section">
<view class="search-bar-wrap" @click="goSearch"> <view class="search-bar-wrap" @click="goSearch">
<text class="search-icon">🔍</text> <text class="search-icon">🔍</text>
@ -21,7 +21,7 @@
<scroll-view scroll-y class="main-content" @scrolltolower="loadMore"> <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"> <view class="sub-grid-wrap" v-if="subCategories.length > 0">
@ -82,6 +82,12 @@
<text class="coupon-txt">{{ goods.couponValue }}</text> <text class="coupon-txt">{{ goods.couponValue }}</text>
</view> </view>
</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>
<view class="goods-bottom-info"> <view class="goods-bottom-info">
@ -103,6 +109,7 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
data() { data() {
return { return {
@ -154,12 +161,10 @@
}); });
}, },
getCategoryTabs() { getCategoryTabs() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/superCategory?is_get_second=1&cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/index/superCategory?is_get_second=1&cid=YsWZ21tx', if (res.data) {
success: (res) => {
if (res.data && res.data.code === 200) {
// navList // navList
const categories = res.data.data.map(item => ({ const categories = res.data.map(item => ({
name: item.name, name: item.name,
cat_id: item.cat_id, cat_id: item.cat_id,
second: item.second || [] second: item.second || []
@ -171,7 +176,6 @@
this.subCategories = currentCat.second; this.subCategories = currentCat.second;
} }
} }
}
}); });
}, },
getProducts(refresh = false) { getProducts(refresh = false) {
@ -191,16 +195,15 @@
sortParam = this.priceOrder === 'asc' ? 8 : 9; sortParam = this.priceOrder === 'asc' ? 8 : 9;
} }
uni.request({ 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 => {
url: `https://api.cmspro.haodanku.com/find/allItemList?category_id=${this.currentCatId}&page=${this.page}&sort=${sortParam}&page_size=20&cid=YsWZ21tx`, if (res.data && res.data.item_info) {
success: (res) => { const list = res.data.item_info.map(item => ({
if (res.data && res.data.code === 200 && res.data.data && res.data.data.item_info) {
const list = res.data.data.item_info.map(item => ({
id: item.id, id: item.id,
image: item.itempic, image: item.itempic,
title: item.itemshorttitle && item.itemshorttitle.length > 17 ? item.itemshorttitle.substring(0, 17) + '...' : item.itemshorttitle, title: item.itemshorttitle && item.itemshorttitle.length > 17 ? item.itemshorttitle.substring(0, 17) + '...' : item.itemshorttitle,
finalPrice: item.itemendprice, finalPrice: item.itemendprice,
couponValue: item.couponmoney || 0, couponValue: item.couponmoney || 0,
tkmoney: item.tkmoney || 0,
sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale, sales: item.itemsale >= 10000 ? (item.itemsale / 10000).toFixed(1) + '万' : item.itemsale,
shopType: item.shoptype === 'B' ? '天猫' : '淘宝', shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
shopName: item.shopname, shopName: item.shopname,
@ -221,10 +224,8 @@
} else { } else {
this.finished = true; this.finished = true;
} }
}, }).finally(() => {
complete: () => {
this.loading = false; this.loading = false;
}
}); });
}, },
switchCategory(item) { switchCategory(item) {
@ -625,6 +626,23 @@
padding: 0 8rpx; 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 { .goods-bottom-info {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -2,7 +2,7 @@
<view class="detail-page-container"> <view class="detail-page-container">
<!-- 沉浸式顶部标题栏 --> <!-- 沉浸式顶部标题栏 -->
<view class="nav-header"> <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="header-content">
<view class="back-area" @click="goBack"> <view class="back-area" @click="goBack">
<text class="back-icon"></text> <text class="back-icon"></text>
@ -16,7 +16,7 @@
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore"> <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"> <view class="filter-bar">
@ -33,26 +33,33 @@
<!-- 高级筛选标签栏 --> <!-- 高级筛选标签栏 -->
<view class="sub-filter-row"> <view class="sub-filter-row">
<view class="sub-filter-item" :class="{ 'active': filterHasCoupon }" @click="toggleFilter('coupon')">有券</view> <view class="sub-filter-item" :class="{ 'active': filterHasCoupon }" @click="toggleFilter('coupon')">有券
<view class="sub-filter-item" :class="{ 'active': filterIsFlagship }" @click="toggleFilter('flagship')">旗舰店</view> </view>
<view class="sub-filter-item" :class="{ 'active': filterIsTmall }" @click="toggleFilter('tmall')">天猫</view> <view class="sub-filter-item" :class="{ 'active': filterIsFlagship }" @click="toggleFilter('flagship')">
<view class="sub-filter-item" :class="{ 'active': filterIsBrand }" @click="toggleFilter('brand')">品牌</view> 旗舰店</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>
<!-- 商品列表 (横向单列风格) --> <!-- 商品列表 (横向单列风格) -->
<view class="goods-list"> <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"> <view class="g-img-left">
<image class="goods-img" :src="goods.image" mode="aspectFill"></image> <image class="goods-img" :src="goods.image" mode="aspectFill"></image>
</view> </view>
<view class="goods-info"> <view class="goods-info">
<view class="goods-title-row"> <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> <text class="title-text">{{ goods.title }}</text>
</view> </view>
<!-- 标签组 --> <!-- 标签组 -->
<view class="labels-row" v-if="goods.labels && goods.labels.length > 0"> <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>
<view class="goods-price-section"> <view class="goods-price-section">
@ -65,6 +72,12 @@
<text class="coupon-txt">{{ goods.couponValue }}</text> <text class="coupon-txt">{{ goods.couponValue }}</text>
</view> </view>
</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>
<view class="goods-bottom-info"> <view class="goods-bottom-info">
@ -85,17 +98,20 @@
<!-- 推荐商品列表 --> <!-- 推荐商品列表 -->
<view class="goods-list recommend-list" v-if="recommendList.length > 0"> <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"> <view class="g-img-left">
<image class="goods-img" :src="goods.image" mode="aspectFill"></image> <image class="goods-img" :src="goods.image" mode="aspectFill"></image>
</view> </view>
<view class="goods-info"> <view class="goods-info">
<view class="goods-title-row"> <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> <text class="title-text">{{ goods.title }}</text>
</view> </view>
<view class="labels-row" v-if="goods.labels && goods.labels.length > 0"> <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>
<view class="goods-price-section"> <view class="goods-price-section">
<view class="price-main"> <view class="price-main">
@ -107,6 +123,12 @@
<text class="coupon-txt">{{ goods.couponValue }}</text> <text class="coupon-txt">{{ goods.couponValue }}</text>
</view> </view>
</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>
<view class="goods-bottom-info"> <view class="goods-bottom-info">
<text class="sales">已售{{ goods.sales }}</text> <text class="sales">已售{{ goods.sales }}</text>
@ -129,6 +151,7 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
data() { data() {
return { return {
@ -195,17 +218,20 @@
filterParams += `&shoptype=${shopTypes.join(',')}`; filterParams += `&shoptype=${shopTypes.join(',')}`;
} }
uni.request({ http.get(
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}`, `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) => { ).then(res => {
if (res.data && res.data.code === 200 && res.data.data && res.data.data.item_info) { if (res.data && res.data.item_info) {
const list = res.data.data.item_info.map(item => ({ const list = res.data.item_info.map(item => ({
id: item.id, id: item.id,
image: item.itempic, 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, finalPrice: item.itemendprice,
couponValue: item.couponmoney || 0, 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' ? '天猫' : '淘宝', shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
shopName: item.shopname, shopName: item.shopname,
labels: item.label || [] labels: item.label || []
@ -229,10 +255,8 @@
this.finished = true; this.finished = true;
if (refresh) this.getRecommendList(); if (refresh) this.getRecommendList();
} }
}, }).finally(() => {
complete: () => {
this.loading = false; this.loading = false;
}
}); });
}, },
getRecommendList() { getRecommendList() {
@ -244,27 +268,33 @@
if (this.filterIsFlagship) recommendParams += '&is_tmall=1'; if (this.filterIsFlagship) recommendParams += '&is_tmall=1';
if (this.filterIsTmall) recommendParams += '&min_id=1'; if (this.filterIsTmall) recommendParams += '&min_id=1';
uni.request({ http.get(
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}`, `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) => { ).then(res => {
if (res.data && res.data.code === 200 && res.data.data) { console.log('res', res)
const list = res.data.data.map(item => ({ if (res.data) {
const list = res.data.map(item => {
console.log('推荐商品:', item.id || item.itemid)
return {
id: item.id || item.itemid, id: item.id || item.itemid,
image: item.itempic, 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, finalPrice: item.itemendprice,
couponValue: item.couponmoney || 0, 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' ? '天猫' : '淘宝', shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
shopName: item.shopname, shopName: item.shopname,
labels: item.label || (item.couponmoney > 0 ? [`${item.couponmoney}元券`] : []) labels: item.label || (item.couponmoney > 0 ? [`${item.couponmoney}元券`] :
})); [])
}
});
this.recommendList = list; this.recommendList = list;
} }
}, }).finally(() => {
complete: () => {
this.loadingRecommend = false; this.loadingRecommend = false;
}
}); });
}, },
changeSort(type) { changeSort(type) {
@ -287,9 +317,10 @@
loadMore() { loadMore() {
this.getProducts(); this.getProducts();
}, },
goToDetail(id) { goToDetail(key, id) {
// console.log(`/pages/detail/detail?${key}=${id}`)
uni.navigateTo({ uni.navigateTo({
url: `/pages/detail/detail?id=${id}` url: `/pages/detail/detail?${key}=${id}`
}); });
} }
} }
@ -544,7 +575,7 @@
.coupon-icon { .coupon-icon {
font-size: 20rpx; font-size: 20rpx;
color: #ffffff; color: #ffffff;
background: rgba(255,255,255,0.2); background: rgba(255, 255, 255, 0.2);
padding: 0 6rpx; padding: 0 6rpx;
height: 100%; height: 100%;
display: flex; display: flex;
@ -557,6 +588,23 @@
padding: 0 8rpx; 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 { .goods-bottom-info {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,7 +1,7 @@
<template> <template>
<view class="classify-container"> <view class="classify-container">
<view class="classify-header"> <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="header-content">
<view class="search-bar" @click="goToSearch"> <view class="search-bar" @click="goToSearch">
<text class="search-icon">🔍</text> <text class="search-icon">🔍</text>
@ -10,7 +10,7 @@
</view> </view>
</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"> <scroll-view scroll-y class="side-bar" :show-scrollbar="false">
<view class="side-item" :class="{ 'active': activeIndex === index }" <view class="side-item" :class="{ 'active': activeIndex === index }"
@ -47,6 +47,7 @@
<script> <script>
import BottomNav from '@/components/bottom-nav/bottom-nav.vue'; import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
import http from '@/request/request.js';
export default { export default {
components: { components: {
@ -75,29 +76,23 @@
}, },
methods: { methods: {
getSuperCategory() { getSuperCategory() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/superCategory?cid=YsWZ21tx', undefined, {
url: 'https://api.cmspro.haodanku.com/index/superCategory?cid=YsWZ21tx',
header: { header: {
'accept': 'application/json, text/plain, */*', 'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9' 'accept-language': 'zh-CN,zh;q=0.9'
}, }
success: (res) => { }).then(res => {
if (res.data && res.data.code === 200) { this.categoryData = res.data;
this.categoryData = res.data.data;
// //
this.matchNavActive(); this.matchNavActive();
}
}
}); });
}, },
matchNavActive() { matchNavActive() {
// //
uni.request({ http.get('https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/bottomBar/lists?cid=YsWZ21tx', if (res.data && res.data.bottom_bar) {
success: (res) => {
if (res.data && res.data.code === 200 && res.data.data.bottom_bar) {
const allowedTitles = ['首页', '榜单', '分类']; const allowedTitles = ['首页', '榜单', '分类'];
const list = res.data.data.bottom_bar const list = res.data.bottom_bar
.filter(item => allowedTitles.includes(item.title)) .filter(item => allowedTitles.includes(item.title))
.sort((a, b) => b.sort - a.sort); .sort((a, b) => b.sort - a.sort);
const idx = list.findIndex(item => item.title === '分类'); const idx = list.findIndex(item => item.title === '分类');
@ -105,7 +100,6 @@
this.navActiveIndex = idx; this.navActiveIndex = idx;
} }
} }
}
}); });
}, },
switchMainCategory(index) { switchMainCategory(index) {

View File

@ -1,14 +1,16 @@
<template> <template>
<view class="detail-container"> <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"> <view class="header-content">
<text class="header-title">商品详情</text> <text class="header-title">商品详情</text>
</view> </view>
</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 class="back-icon-box" :style="{ borderColor: iconColor }"></view>
</view> </view>
@ -28,7 +30,7 @@
<!-- 活动横幅 --> <!-- 活动横幅 -->
<view class="activity-banner" v-if="product.activity && product.activity.app_img"> <view class="activity-banner" v-if="product.activity && product.activity.app_img">
<image :src="product.activity.app_img" mode="widthFix" class="banner-img"></image> <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> </view>
<!-- 热销榜单提示 --> <!-- 热销榜单提示 -->
@ -66,6 +68,9 @@
<view class="tags-row"> <view class="tags-row">
<text class="tag-capsule" v-for="(tag, index) in product.labels" :key="index">{{ tag }}</text> <text class="tag-capsule" v-for="(tag, index) in product.labels" :key="index">{{ tag }}</text>
</view> </view>
<view class="estimate-tip" v-if="!$store.state.isThirdParty">
<text>消费券{{ $estimateCoupon(product.commission) }}为预估值实际以系统发放为准</text>
</view>
</view> </view>
<!-- 优惠券模块 --> <!-- 优惠券模块 -->
@ -78,11 +83,11 @@
</view> </view>
<view class="coupon-time">使用期限: {{ product.couponTime }}</view> <view class="coupon-time">使用期限: {{ product.couponTime }}</view>
</view> </view>
<view class="coupon-right"> <!-- <view class="coupon-right">
<view class="get-btn" @click="goBuy">立即领券</view> <view class="get-btn" @click="goBuy">立即领券</view>
</view> </view> -->
<view class="coupon-circle top"></view> <!-- <view class="coupon-circle top"></view> -->
<view class="coupon-circle bottom"></view> <!-- <view class="coupon-circle bottom"></view> -->
</view> </view>
</view> </view>
@ -158,7 +163,7 @@
</view> </view>
<view class="list"> <view class="list">
<view class="list-container"> <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"> <view class="shop-img">
<image :src="item.itempic" mode="aspectFill" lazy-load></image> <image :src="item.itempic" mode="aspectFill" lazy-load></image>
</view> </view>
@ -178,6 +183,12 @@
<text>{{ item.couponmoney }}</text> <text>{{ item.couponmoney }}</text>
</view> </view>
</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="flex-left flex-left-t">
<view class="sales-volume">已售{{ formatSales(item.itemsale) }}</view> <view class="sales-volume">已售{{ formatSales(item.itemsale) }}</view>
</view> </view>
@ -225,7 +236,7 @@
</view> </view>
<view class="bar-right"> <view class="bar-right">
<view class="copy-btn" @click="copyTaoWord">复制口令购买</view> <view class="copy-btn" @click="copyTaoWord">复制口令购买</view>
<view class="buy-btn" @click="goBuy">领券购买</view> <!-- <view class="buy-btn" @click="goBuy">领券购买</view> -->
</view> </view>
</view> </view>
</view> </view>
@ -272,17 +283,24 @@
}, },
onLoad(options) { onLoad(options) {
this.product.id = options.id || '55493973'; this.product.id = options.id || '55493973';
this.keywordid = options.keywordid || '';
const sysInfo = uni.getSystemInfoSync(); const sysInfo = uni.getSystemInfoSync();
this.statusBarHeight = sysInfo.statusBarHeight || 44; this.statusBarHeight = sysInfo.statusBarHeight || 44;
this.getDetailData(); this.getDetailData();
}, },
methods: { methods: {
getDetailData() { getDetailData() {
const useSuperSearch = this.keywordid;
uni.request({ 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) => { success: (res) => {
if (res.data && res.data.code === 200) { 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); console.log('详情数据获取成功:', d);
// //
@ -379,12 +397,19 @@
this.getSimilarProducts(d.itemid, d.son_category); this.getSimilarProducts(d.itemid, d.son_category);
this.isLoaded = true; this.isLoaded = true;
// iOS
this.fetchTaoWord();
} }
} }
}); });
}, },
fetchTaoWord(callback) { fetchTaoWord(callback) {
if (!this.product.taobaoId) return; if (!this.product.taobaoId) return;
// iOS
if (this.product.taoCode || this.product.couponUrl) {
if (callback) callback();
return;
}
uni.showLoading({ title: '加载中...', mask: true }); uni.showLoading({ title: '加载中...', mask: true });
uni.request({ uni.request({
url: 'https://v2.api.haodanku.com/ratesurl', url: 'https://v2.api.haodanku.com/ratesurl',
@ -398,7 +423,7 @@
get_taoword: 1, get_taoword: 1,
pid: 'mm_284380119_1881450385_111415850448', pid: 'mm_284380119_1881450385_111415850448',
tb_name: 'michuan2018', tb_name: 'michuan2018',
relation_id: '2864040832', relation_id: this.$store.getters.relationId || '',
apikey: '5417B681C5EA' apikey: '5417B681C5EA'
}, },
success: (res) => { success: (res) => {
@ -423,7 +448,10 @@
url: `https://api.cmspro.haodanku.com/detail/getRecommendItems?itemid=${itemId}&son_category=${sonCategory}&cid=YsWZ21tx`, url: `https://api.cmspro.haodanku.com/detail/getRecommendItems?itemid=${itemId}&son_category=${sonCategory}&cid=YsWZ21tx`,
success: (res) => { success: (res) => {
if (res.data && res.data.code === 200) { 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; return sale;
}, },
goToDetail(id) { goToDetail(id) {
// console.log(id)
uni.navigateTo({ uni.navigateTo({
url: `/pages/detail/detail?id=${id}` url: `/pages/detail/detail?id=${id}`
}); });
@ -466,17 +495,23 @@
uni.showToast({ title: '暂无领券链接', icon: 'none' }); uni.showToast({ title: '暂无领券链接', icon: 'none' });
return; return;
} }
// #ifdef H5
window.location.href = url;
// #endif
// #ifndef H5
uni.setClipboardData({ uni.setClipboardData({
data: url, data: url,
success: () => { success: () => {
uni.showToast({ title: '链接已复制,请到浏览器打开', icon: 'none' }); 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) { handleScroll(e) {
@ -491,7 +526,7 @@
this.iconColor = ratio > 0.5 ? '#333333' : '#ffffff'; this.iconColor = ratio > 0.5 ? '#333333' : '#ffffff';
}, },
goHome() { goHome() {
uni.switchTab({ uni.redirectTo({
url: '/pages/index/index' url: '/pages/index/index'
}); });
}, },
@ -880,6 +915,22 @@
padding: 0 8rpx; 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 { .sales-volume {
font-size: 22rpx; font-size: 22rpx;
color: #999; color: #999;
@ -1074,6 +1125,15 @@
margin-bottom: 10rpx; margin-bottom: 10rpx;
} }
.estimate-tip {
font-size: 22rpx;
color: #ff8a00;
background-color: #fff8f0;
padding: 8rpx 16rpx;
border-radius: 8rpx;
margin-top: 16rpx;
}
/* 优惠券 */ /* 优惠券 */
.coupon-section { .coupon-section {
padding: 0 30rpx; padding: 0 30rpx;
@ -1610,7 +1670,8 @@
.copy-btn { .copy-btn {
flex: 1; flex: 1;
background-color: #ffbcba; /* background-color: #ffbcba; */
background: linear-gradient(to right, #ff715a, #ff416c);
color: #ffffff; color: #ffffff;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,8 +1,8 @@
<template> <template>
<view class="container"> <view class="container">
<!-- 头部搜索与导航 --> <!-- 头部搜索与导航 -->
<!-- :style="{ paddingTop: statusBarHeight + 'px' }" -->
<view class="header"> <view class="header">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="search-section"> <view class="search-section">
<view class="search-bar-wrap" @click="goSearch"> <view class="search-bar-wrap" @click="goSearch">
<text class="search-icon">🔍</text> <text class="search-icon">🔍</text>
@ -18,8 +18,8 @@
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<!-- :style="{ paddingTop: headerHeight + 'px' }" -->
<scroll-view scroll-y class="main-content" :show-scrollbar="false" :scroll-into-view="scrollTarget" scroll-with-animation> <view class="main-content" :style="{ paddingTop: '120px' }">
<!-- 轮播图区域 --> <!-- 轮播图区域 -->
<view class="banner-box" id="top-section"> <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"> <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>
<view class="g-coupon-tag"> {{ goods.couponValue }}</view> <view class="g-coupon-tag"> {{ goods.couponValue }}</view>
</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 class="goods-sales">已售 {{ goods.sales }} </view>
</view> </view>
</view> </view>
</view> </view>
<view class="loading-more">-- 到底啦 --</view> <view class="loading-more">-- 到底啦 --</view>
</scroll-view> </view>
<!-- 通用底部 Tab --> <!-- 通用底部 Tab -->
<bottom-nav :activeTab="0"></bottom-nav> <bottom-nav :activeTab="0"></bottom-nav>
@ -255,6 +261,7 @@
<script> <script>
import BottomNav from '@/components/bottom-nav/bottom-nav.vue'; import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
import http from '@/request/request.js';
export default { export default {
components: { components: {
@ -266,7 +273,6 @@
activeBottomTab: 0, activeBottomTab: 0,
currentTab: 0, currentTab: 0,
navList: [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }], navList: [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }],
scrollTarget: '',
banners: [ banners: [
'https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&q=80', 'https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&q=80',
'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=800&q=80', 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=800&q=80',
@ -313,6 +319,17 @@
this.getWorthBuyLists(); this.getWorthBuyLists();
this.getBrandSaleList(); this.getBrandSaleList();
}, },
onPullDownRefresh() {
this.getIndexData();
this.getCategoryList();
this.getNoticeList();
this.getGoodsList();
this.getWorthBuyLists();
this.getBrandSaleList();
setTimeout(() => {
uni.stopPullDownRefresh();
}, 600);
},
computed: { computed: {
gridMenuPages() { gridMenuPages() {
const pages = []; const pages = [];
@ -344,11 +361,8 @@
}); });
}, },
getIndexData() { getIndexData() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/index?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/index/index?cid=YsWZ21tx', const d = res.data;
success: (res) => {
if (res.data && res.data.code === 200 && res.data.data) {
const d = res.data.data;
if (Array.isArray(d.navs)) { if (Array.isArray(d.navs)) {
this.menus = d.navs; this.menus = d.navs;
} }
@ -360,46 +374,40 @@
if (Array.isArray(d.tile_long) && d.tile_long.length > 0) { if (Array.isArray(d.tile_long) && d.tile_long.length > 0) {
this.adList = d.tile_long.map(t => t.img).filter(Boolean); this.adList = d.tile_long.map(t => t.img).filter(Boolean);
} }
} }).catch(err => {
} console.error('获取首页数据失败:', err.message);
}); });
}, },
goToDetail(id) { goToDetail(id) {
console.log('正在跳转到详情页商品ID:', id); console.log('正在跳转到详情页商品ID:', id);
uni.navigateTo({ uni.navigateTo({
url: `/pages/detail/detail?id=${id}` url: `/pages/detail/detail?id=${id}`
// url: `/pages/activity/activity`
}); });
}, },
getCategoryList() { getCategoryList() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/category?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/index/category?cid=YsWZ21tx', if (res.data.category) {
success: (res) => { this.navList = [{ name: '首页', cat_id: 0 }, { name: '推荐', cat_id: -1 }, ...res.data.category];
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];
}
} }
}).catch(err => {
console.error('获取分类失败:', err.message);
}); });
}, },
getNoticeList() { getNoticeList() {
uni.request({ http.get('https://api.cmspro.haodanku.com/msg/getMsgs?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/msg/getMsgs?cid=YsWZ21tx', this.noticeList = res.data.msgs;
success: (res) => { if (res.data.num) {
if (res.data && res.data.code === 200) { this.qiangNum = res.data.num;
this.noticeList = res.data.data.msgs;
if (res.data.data.num) {
this.qiangNum = res.data.data.num;
}
}
} }
}).catch(err => {
console.error('获取通知失败:', err.message);
}); });
}, },
getGoodsList() { getGoodsList() {
uni.request({ http.get('https://api.cmspro.haodanku.com/recommend/getRecommend?page_size=200&page=1&type=2&cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/recommend/getRecommend?page_size=200&page=1&type=2&cid=YsWZ21tx', const list = res.data.recommends.map(item => {
success: (res) => {
if (res.data && res.data.code === 200) {
const list = res.data.data.recommends.map(item => {
// //
let salesStr = item.itemsale; let salesStr = item.itemsale;
if (item.itemsale >= 10000) { if (item.itemsale >= 10000) {
@ -415,19 +423,17 @@
sales: salesStr, sales: salesStr,
brandTag: item.brand_name, brandTag: item.brand_name,
lowestTag: item.label && item.label.length > 0 ? item.label[0] : '', lowestTag: item.label && item.label.length > 0 ? item.label[0] : '',
shopType: item.shoptype === 'B' ? '天猫' : '淘宝' shopType: item.shoptype === 'B' ? '天猫' : '淘宝',
tkmoney: item.tkmoney
}; };
}); });
this.goodsList = list; this.goodsList = list;
} }).catch(err => {
} console.error('获取商品列表失败:', err.message);
}); });
}, },
getWorthBuyLists() { getWorthBuyLists() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/deserveLists?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/index/deserveLists?cid=YsWZ21tx',
success: (res) => {
if (res.data && res.data.code === 200) {
const formatData = (list) => { const formatData = (list) => {
return list.map(item => ({ return list.map(item => ({
id: item.id, id: item.id,
@ -443,19 +449,17 @@
shopType: item.shoptype === 'B' ? '天猫' : '淘宝' shopType: item.shoptype === 'B' ? '天猫' : '淘宝'
})); }));
}; };
this.deserveList = formatData(res.data.data.deserve_lists); this.deserveList = formatData(res.data.deserve_lists);
this.nineList = formatData(res.data.data.nine_lists); this.nineList = formatData(res.data.nine_lists);
this.nineteenList = formatData(res.data.data.nineteen_lists); this.nineteenList = formatData(res.data.nineteen_lists);
} }).catch(err => {
} console.error('获取值得买列表失败:', err.message);
}); });
}, },
getBrandSaleList() { getBrandSaleList() {
uni.request({ http.get('https://api.cmspro.haodanku.com/brandItem/choiceness?cid=YsWZ21tx').then(res => {
url: 'https://api.cmspro.haodanku.com/brandItem/choiceness?cid=YsWZ21tx', if (res.data.brand_prefecture) {
success: (res) => { this.brandSaleShops = res.data.brand_prefecture.map(shop => ({
if (res.data && res.data.code === 200 && res.data.data.brand_prefecture) {
this.brandSaleShops = res.data.data.brand_prefecture.map(shop => ({
bg: shop.backimage || 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&q=80', 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', logo: shop.brand_logo || 'https://images.unsplash.com/photo-1560155016-bd4879ae8f21?w=200&q=80',
name: shop.fq_brand_name || '大牌特卖', name: shop.fq_brand_name || '大牌特卖',
@ -472,7 +476,8 @@
})) }))
})); }));
} }
} }).catch(err => {
console.error('获取品牌特卖失败:', err.message);
}); });
}, },
switchTab(index) { switchTab(index) {
@ -489,13 +494,9 @@
} }
this.currentTab = index; this.currentTab = index;
this.scrollTarget = ''; // uni.pageScrollTo({
this.$nextTick(() => { selector: tabName === '推荐' ? '#goods-list-section' : '#top-section',
if (tabName === '推荐') { duration: 300
this.scrollTarget = 'goods-list-section';
} else if (tabName === '首页') {
this.scrollTarget = 'top-section';
}
}); });
}, },
goToChoiceness() { goToChoiceness() {
@ -515,14 +516,15 @@
<style scoped> <style scoped>
.container { .container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f6f8; background-color: #f5f6f8;
} }
/* 头部区域 */ /* 头部区域 */
.header { .header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ffffff; background: #ffffff;
padding-bottom: 10rpx; padding-bottom: 10rpx;
z-index: 100; z-index: 100;
@ -620,13 +622,12 @@
/* 主体滚动区域 */ /* 主体滚动区域 */
.main-content { .main-content {
flex: 1; min-height: 100vh;
overflow-y: auto;
} }
/* 轮播图 */ /* 轮播图 */
.banner-box { .banner-box {
padding: 20rpx 30rpx 10rpx; padding: 0 30rpx 10rpx;
} }
.swiper { .swiper {
@ -1625,6 +1626,23 @@
border-radius: 4rpx; 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 { .goods-sales {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;

View File

@ -2,7 +2,7 @@
<view class="rank-container"> <view class="rank-container">
<!-- 沉浸式渐变头部 --> <!-- 沉浸式渐变头部 -->
<view class="rank-header"> <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="rank-main-tabs">
<view class="main-tab" :class="{ 'active': rankType === 1 }" @click="switchRankType(1)"> <view class="main-tab" :class="{ 'active': rankType === 1 }" @click="switchRankType(1)">
@ -30,7 +30,7 @@
<scroll-view scroll-y class="rank-content" @scrolltolower="loadMore"> <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 样式) --> <!-- Top 1-3 领奖台布局 (参考 shop-sy2 样式) -->
<view class="top-three-wrap" v-if="goodsList.length >= 3"> <view class="top-three-wrap" v-if="goodsList.length >= 3">
@ -147,6 +147,7 @@
<script> <script>
import BottomNav from '@/components/bottom-nav/bottom-nav.vue'; import BottomNav from '@/components/bottom-nav/bottom-nav.vue';
import http from '@/request/request.js';
export default { export default {
components: { components: {
@ -194,18 +195,16 @@
if (this.loading) return; if (this.loading) return;
this.loading = true; this.loading = true;
uni.request({ http.get('https://api.cmspro.haodanku.com/ranking/lists', {
url: 'https://api.cmspro.haodanku.com/ranking/lists',
data: {
type: this.rankType, type: this.rankType,
category_id: this.currentCateId, category_id: this.currentCateId,
page: this.page, page: this.page,
page_size: 60, page_size: 60,
cid: 'YsWZ21tx' cid: 'YsWZ21tx'
}, })
success: (res) => { .then(res => {
if (res.data && res.data.code === 200 && res.data.data && res.data.data.list_item && res.data.data.list_item.list) { if (res.data && res.data.list_item && res.data.list_item.list) {
const list = res.data.data.list_item.list.map(item => { const list = res.data.list_item.list.map(item => {
// //
let title = item.itemshorttitle || item.itemtitle; let title = item.itemshorttitle || item.itemtitle;
if (title.length > 18) { if (title.length > 18) {
@ -221,7 +220,7 @@
let itemSale2Str = itemSale2 >= 10000 ? (itemSale2 / 10000).toFixed(1) + '万' : itemSale2; let itemSale2Str = itemSale2 >= 10000 ? (itemSale2 / 10000).toFixed(1) + '万' : itemSale2;
return { return {
id: item.itemid, id: item.id,
image: item.itempic, image: item.itempic,
title: title, title: title,
finalPrice: item.itemendprice, finalPrice: item.itemendprice,
@ -235,10 +234,9 @@
}); });
this.goodsList = [...this.goodsList, ...list]; this.goodsList = [...this.goodsList, ...list];
} }
}, })
complete: () => { .finally(() => {
this.loading = false; this.loading = false;
}
}); });
}, },
loadMore() { loadMore() {

View File

@ -2,7 +2,7 @@
<view class="save-money-container"> <view class="save-money-container">
<!-- 沉浸式状态栏与高定自定义顶部导航条 --> <!-- 沉浸式状态栏与高定自定义顶部导航条 -->
<view class="custom-nav-bar"> <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-header">
<view class="nav-back" @click="goBack"> <view class="nav-back" @click="goBack">
<text class="back-arrow"></text> <text class="back-arrow"></text>
@ -13,7 +13,7 @@
</view> </view>
<!-- 占位符防止图片被顶部悬浮遮挡 --> <!-- 占位符防止图片被顶部悬浮遮挡 -->
<view class="nav-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view> <view class="nav-placeholder" :style="{ height: '44px' }"></view>
<!-- 核心内容区无缝连接的两张全景自适应介绍照片 --> <!-- 核心内容区无缝连接的两张全景自适应介绍照片 -->
<view class="photo-flow"> <view class="photo-flow">

View File

@ -2,7 +2,7 @@
<view class="search-container"> <view class="search-container">
<!-- 头部搜索栏 --> <!-- 头部搜索栏 -->
<view class="search-header"> <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="header-main">
<view class="back-btn" @click="goBack" hover-class="back-btn-hover"> <view class="back-btn" @click="goBack" hover-class="back-btn-hover">
<text class="back-arrow"></text> <text class="back-arrow"></text>
@ -24,7 +24,7 @@
</view> </view>
</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"> @scrolltolower="onScrollBottom">
<!-- 搜索结果列表 --> <!-- 搜索结果列表 -->
<block v-if="isSearching"> <block v-if="isSearching">
@ -73,6 +73,12 @@
<text class="coupon-txt">{{ goods.couponValue }}</text> <text class="coupon-txt">{{ goods.couponValue }}</text>
</view> </view>
</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>
<view class="goods-bottom-info"> <view class="goods-bottom-info">
<text class="sales">已售{{ goods.sales }}</text> <text class="sales">已售{{ goods.sales }}</text>
@ -174,6 +180,7 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
data() { data() {
return { return {
@ -250,22 +257,18 @@
uni.setStorageSync('search_history', JSON.stringify(list)); uni.setStorageSync('search_history', JSON.stringify(list));
}, },
getRankingList() { getRankingList() {
uni.request({ http.get('https://api.cmspro.haodanku.com/search/searchRankingList?cid=YsWZ21tx')
url: 'https://api.cmspro.haodanku.com/search/searchRankingList?cid=YsWZ21tx', .then(res => {
success: (res) => { if (res.data && res.data.ranking_list) {
if (res.data && res.data.code === 200 && res.data.data.ranking_list) { this.rankingList = res.data.ranking_list.slice(0, 10);
this.rankingList = res.data.data.ranking_list.slice(0, 10);
}
} }
}); });
}, },
getHotThemes() { getHotThemes() {
uni.request({ http.get('https://api.cmspro.haodanku.com/index/hotTheme?cid=YsWZ21tx')
url: 'https://api.cmspro.haodanku.com/index/hotTheme?cid=YsWZ21tx', .then(res => {
success: (res) => { if (res.data) {
if (res.data && res.data.code === 200) { this.hotThemes = res.data.slice(0, 10);
this.hotThemes = res.data.data.slice(0, 10);
}
} }
}); });
}, },
@ -317,11 +320,9 @@
if (this.filterIsTmall) shopTypes.push(2); if (this.filterIsTmall) shopTypes.push(2);
if (shopTypes.length > 0) data.shoptype = shopTypes.join(','); if (shopTypes.length > 0) data.shoptype = shopTypes.join(',');
uni.request({ http.get('https://api.cmspro.haodanku.com/find/allItemList', data)
url: 'https://api.cmspro.haodanku.com/find/allItemList', .then(res => {
data, const body = res.body || {};
success: (res) => {
const body = res.data || {};
if (body.code === 200 && body.data) { if (body.code === 200 && body.data) {
if (body.data.num_page) this.numPage = body.data.num_page; if (body.data.num_page) this.numPage = body.data.num_page;
const rawList = body.data.item_info || []; const rawList = body.data.item_info || [];
@ -334,6 +335,7 @@
: (item.itemshorttitle || item.itemtitle), : (item.itemshorttitle || item.itemtitle),
finalPrice: item.itemendprice, finalPrice: item.itemendprice,
couponValue: item.couponmoney || 0, couponValue: item.couponmoney || 0,
tkmoney: item.tkmoney || 0,
sales: item.itemsale >= 10000 sales: item.itemsale >= 10000
? (item.itemsale / 10000).toFixed(1) + '万' ? (item.itemsale / 10000).toFixed(1) + '万'
: item.itemsale, : item.itemsale,
@ -359,13 +361,13 @@
uni.showToast({ title: body.msg || '搜索失败', icon: 'none' }); uni.showToast({ title: body.msg || '搜索失败', icon: 'none' });
} }
} }
}, })
fail: () => { .catch(err => {
uni.showToast({ title: '网络错误', icon: 'none' }); uni.showToast({ title: err.message || '网络错误', icon: 'none' });
}, this.loading = false;
complete: () => { })
.finally(() => {
this.loading = false; this.loading = false;
}
}); });
}, },
onScrollBottom() { onScrollBottom() {
@ -976,6 +978,23 @@
padding: 0 8rpx; 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 { .goods-bottom-info {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -2,7 +2,7 @@
<view class="choiceness-container"> <view class="choiceness-container">
<!-- 自定义沉浸式头部导航 --> <!-- 自定义沉浸式头部导航 -->
<view class="custom-header"> <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-bar">
<view class="nav-left back-area" @click="goBack"> <view class="nav-left back-area" @click="goBack">
<text class="back-icon"></text> <text class="back-icon"></text>
@ -13,7 +13,7 @@
</view> </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"> <view class="top-scroll-brands-section">
@ -162,6 +162,8 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
data() { data() {
return { return {
@ -443,11 +445,10 @@
fetchTopBrandsData(isLoadMore = false) { fetchTopBrandsData(isLoadMore = false) {
if (this.topLoading) return; if (this.topLoading) return;
this.topLoading = true; this.topLoading = true;
uni.request({ http.get(`https://api.cmspro.haodanku.com/brandItem/getBrands?page=${this.topPage}&page_size=20&cid=YsWZ21tx`)
url: `https://api.cmspro.haodanku.com/brandItem/getBrands?page=${this.topPage}&page_size=20&cid=YsWZ21tx`, .then((res) => {
success: (res) => { if (res.data && Array.isArray(res.data.list)) {
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.list)) { const list = res.data.list.map(item => ({
const list = res.data.data.list.map(item => ({
id: item.id, id: item.id,
fq_brand_name: item.fq_brand_name, fq_brand_name: item.fq_brand_name,
brand_logo: item.brand_logo ? item.brand_logo.replace('http://', 'https://') : '' 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) { if (pagination && this.topPage >= pagination.page_count) {
this.topFinished = true; this.topFinished = true;
} else if (list.length < 20) { } else if (list.length < 20) {
@ -472,27 +473,25 @@
this.topFinished = true; this.topFinished = true;
} }
} }
}, })
fail: (err) => { .catch((err) => {
console.log('拉取上方滚动品牌接口异常', err); console.log('拉取上方滚动品牌接口异常', err);
if (isLoadMore && this.topPage > 1) { if (isLoadMore && this.topPage > 1) {
this.topPage -= 1; this.topPage -= 1;
} }
}, })
complete: () => { .finally(() => {
this.topLoading = false; this.topLoading = false;
}
}); });
}, },
// //
fetchChoicenessData() { fetchChoicenessData() {
uni.request({ http.get('https://api.cmspro.haodanku.com/brandItem/choiceness?is_get_category=1&cid=YsWZ21tx')
url: 'https://api.cmspro.haodanku.com/brandItem/choiceness?is_get_category=1&cid=YsWZ21tx', .then((res) => {
success: (res) => { if (res.data) {
if (res.data && res.data.code === 200 && res.data.data) {
// 1. / // 1. /
if (res.data.data.category && Array.isArray(res.data.data.category)) { if (res.data.category && Array.isArray(res.data.category)) {
const mappedCats = res.data.data.category.map(c => ({ const mappedCats = res.data.category.map(c => ({
cat_id: Number(c.cat_id), cat_id: Number(c.cat_id),
cat_name: c.cat_name cat_name: c.cat_name
})); }));
@ -500,8 +499,8 @@
} }
// 2. // 2.
if (res.data.data.brand_prefecture && Array.isArray(res.data.data.brand_prefecture)) { if (res.data.brand_prefecture && Array.isArray(res.data.brand_prefecture)) {
const list = res.data.data.brand_prefecture.map(shop => { 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 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'; 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); console.log('拉取线上精选品牌专场分类联动数据失败,采用固定底座显示', err);
}
}); });
}, },
// brandCategory Toast // brandCategory Toast
fetchBrandCategoryData(categoryId) { fetchBrandCategoryData(categoryId) {
uni.request({ http.get(`https://api.cmspro.haodanku.com/brandItem/brandCategory?category_id=${categoryId}&cid=YsWZ21tx`)
url: `https://api.cmspro.haodanku.com/brandItem/brandCategory?category_id=${categoryId}&cid=YsWZ21tx`, .then((res) => {
success: (res) => { if (res.data && Array.isArray(res.data.brands)) {
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.brands)) {
// / // /
const defaultBanners = [ const defaultBanners = [
'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&q=80', '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' '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 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]; let bgStr = defaultBanners[bIndex % defaultBanners.length];
@ -592,19 +589,17 @@
} else { } else {
console.log('拉取分类数据异常或格式不对'); console.log('拉取分类数据异常或格式不对');
} }
}, })
fail: (err) => { .catch((err) => {
console.log('调用 brandCategory 接口失败', err); console.log('调用 brandCategory 接口失败', err);
}
}); });
}, },
// //
fetchBrandSaleData() { fetchBrandSaleData() {
uni.request({ http.get('https://api.cmspro.haodanku.com/brandItem/brandSale?cid=YsWZ21tx')
url: 'https://api.cmspro.haodanku.com/brandItem/brandSale?cid=YsWZ21tx', .then((res) => {
success: (res) => { if (res.data && Array.isArray(res.data.items)) {
if (res.data && res.data.code === 200 && res.data.data && Array.isArray(res.data.data.items)) { const list = res.data.items.map(item => {
const list = res.data.data.items.map(item => {
let logoUrl = ''; let logoUrl = '';
if (item.brand_info && item.brand_info.brand_logo) { if (item.brand_info && item.brand_info.brand_logo) {
logoUrl = item.brand_info.brand_logo.replace('http://', 'https://'); logoUrl = item.brand_info.brand_logo.replace('http://', 'https://');
@ -640,10 +635,9 @@
this.brandSaleList = list; this.brandSaleList = list;
} }
} }
}, })
fail: (err) => { .catch((err) => {
console.log('拉取底部品牌热销接口异常,采用极尽精美的静态底座显示', err); console.log('拉取底部品牌热销接口异常,采用极尽精美的静态底座显示', err);
}
}); });
} }
} }

View File

@ -2,7 +2,7 @@
<view class="brand-detail-container"> <view class="brand-detail-container">
<!-- 自定义沉浸式头部导航 --> <!-- 自定义沉浸式头部导航 -->
<view class="custom-header" :class="{ 'header-scrolled': scrollY > 50 }"> <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-bar">
<view class="nav-left back-area" @click="goBack"> <view class="nav-left back-area" @click="goBack">
<text class="back-icon"></text> <text class="back-icon"></text>
@ -22,7 +22,7 @@
<view class="biography-overlay"></view> <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"> <view class="logo-wrapper">
<image class="brand-logo-img" :src="brandInfo.brand_logo || brandInfo.inside_logo" mode="aspectFit"></image> <image class="brand-logo-img" :src="brandInfo.brand_logo || brandInfo.inside_logo" mode="aspectFit"></image>
</view> </view>
@ -149,6 +149,8 @@
</template> </template>
<script> <script>
import http from '@/request/request.js';
export default { export default {
data() { data() {
return { return {
@ -278,21 +280,20 @@
sortParam = this.priceOrder === 'asc' ? 8 : 9; sortParam = this.priceOrder === 'asc' ? 8 : 9;
} }
uni.request({ http.get(`https://api.cmspro.haodanku.com/brandItem/detail?page=${this.page}&page_size=20&sort=${sortParam}&brand_id=${this.brandId}&cid=YsWZ21tx`)
url: `https://api.cmspro.haodanku.com/brandItem/detail?page=${this.page}&page_size=20&sort=${sortParam}&brand_id=${this.brandId}&cid=YsWZ21tx`, .then((res) => {
success: (res) => { if (res.data) {
if (res.data && res.data.code === 200 && res.data.data) {
// //
if (res.data.data.brand_info) { if (res.data.brand_info) {
const info = res.data.data.brand_info; const info = res.data.brand_info;
if (info.brand_logo) info.brand_logo = info.brand_logo.replace('http://', 'https://'); 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://'); if (info.inside_logo) info.inside_logo = info.inside_logo.replace('http://', 'https://');
this.brandInfo = info; this.brandInfo = info;
} }
// //
if (res.data.data.items && Array.isArray(res.data.data.items.list)) { if (res.data.items && Array.isArray(res.data.items.list)) {
const list = res.data.data.items.list.map(goods => { const list = res.data.items.list.map(goods => {
let pic = goods.itempic ? goods.itempic.replace('http://', 'https://') : ''; let pic = goods.itempic ? goods.itempic.replace('http://', 'https://') : '';
return { return {
id: goods.id, id: goods.id,
@ -315,7 +316,7 @@
this.itemList = list; this.itemList = list;
} }
const pagination = res.data.data.items.pagination; const pagination = res.data.items.pagination;
if (pagination && this.page >= pagination.page_count) { if (pagination && this.page >= pagination.page_count) {
this.finished = true; this.finished = true;
} else if (list.length < 20) { } else if (list.length < 20) {
@ -327,15 +328,14 @@
} else { } else {
uni.showToast({ title: '加载专区数据失败', icon: 'none' }); uni.showToast({ title: '加载专区数据失败', icon: 'none' });
} }
}, })
fail: (err) => { .catch((err) => {
console.log('拉取品牌专页接口错误', err); console.log('拉取品牌专页接口错误', err);
if (isLoadMore && this.page > 1) this.page -= 1; if (isLoadMore && this.page > 1) this.page -= 1;
}, })
complete: () => { .finally(() => {
this.loading = false; this.loading = false;
if (!isLoadMore) uni.hideLoading(); if (!isLoadMore) uni.hideLoading();
}
}); });
} }
} }

257
request/request.js Normal file
View File

@ -0,0 +1,257 @@
/**
* uni.request 统一封装
*
* 设计目标
* 1. 自动兼容项目中所有 API 的成功状态判断code === 200 / code === 1 / status === 200
* 2. 支持自定义 header超时baseURL
* 3. Promise 调用简单
* 4. 支持请求/响应拦截器便于后续统一注入 token处理错误等
* 5. 数据提取自动降级优先取 res.data.data不存在则取 res.data
*/
class Request {
/**
* @param {Object} config - 全局默认配置
* @param {string} config.baseURL - 基础 URL
* @param {number} config.timeout - 超时时间ms默认 30000
* @param {Object} config.header - 默认请求头
*/
constructor(config = {}) {
this.config = {
baseURL: '',
timeout: 30000,
header: {
'Content-Type': 'application/json'
},
...config
};
this.interceptors = {
request: [],
response: []
};
}
/**
* 添加请求拦截器
* @param {Function} fn - (config) => config
*/
addRequestInterceptor(fn) {
if (typeof fn === 'function') {
this.interceptors.request.push(fn);
}
}
/**
* 添加响应拦截器
* @param {Function} fn - (response) => response
*/
addResponseInterceptor(fn) {
if (typeof fn === 'function') {
this.interceptors.response.push(fn);
}
}
/**
* 判断响应是否成功
*
* 兼容项目中实际存在的 4 种响应格式
* 1. CMS API: res.data.code === 200
* 2. Auth API: res.statusCode === 200 && (res.data.status === 200 || res.data.code === 1)
* 3. Hdk v2 API: res.data.code === 1
* 4. 兜底: res.statusCode === 200无业务状态码时
*
* @param {Object} res - uni.request success 回调参数
* @returns {boolean}
*/
isSuccess(res) {
// HTTP 层已失败(如 404/500
if (res.statusCode >= 400) {
return false;
}
const data = res.data;
if (!data || typeof data !== 'object') {
// 无响应体时,以 HTTP 状态码为准
return res.statusCode === 200;
}
// 提取业务状态码(兼容 code / status 两种字段名)
const code = data.code !== undefined ? Number(data.code)
: data.status !== undefined ? Number(data.status)
: null;
if (code !== null && !isNaN(code)) {
return code === 200 || code === 1;
}
// 无业务状态码时,以 HTTP 状态码为准
return res.statusCode === 200;
}
/**
* 从响应中提取业务数据
*
* 降级策略
* - 优先取 res.data.dataCMS 标准格式
* - 不存在则取 res.data 本身直接返回格式
* - 空响应返回 null
*
* @param {Object} res - uni.request success 回调参数
* @returns {any}
*/
extractData(res) {
const data = res.data;
if (!data || typeof data !== 'object') {
return data;
}
return data.data !== undefined ? data.data : data;
}
/**
* 从响应中提取错误信息
* @param {Object} res - uni.request success 回调参数
* @returns {string}
*/
extractError(res) {
const data = res.data;
if (!data || typeof data !== 'object') {
return `请求失败 (HTTP ${res.statusCode})`;
}
return data.msg || data.message || data.error || `请求失败 (code: ${data.code || data.status})`;
}
/**
* 核心请求方法
*
* @param {Object} options - 请求配置
* @param {string} options.url - 请求地址可相对 baseURL
* @param {string} options.method - 请求方法GET/POST/PUT/DELETE默认 GET
* @param {Object} options.data - 请求数据
* @param {Object} options.header - 自定义请求头会合并覆盖默认 header大小写不敏感
* @param {number} options.timeout - 本次请求单独设置的超时时间
* @returns {Promise<{success: true, data: any, body: any, raw: Object}>}
*/
request(options = {}) {
return new Promise((resolve, reject) => {
// 1. 合并配置:默认 → 实例配置 → 单次请求配置
// header 合并需大小写不敏感去重(如 Content-Type 与 content-type 是同一头)
const mergedHeader = {};
for (const [k, v] of Object.entries(this.config.header)) {
mergedHeader[k.toLowerCase()] = v;
}
for (const [k, v] of Object.entries(options.header || {})) {
mergedHeader[k.toLowerCase()] = v;
}
let config = {
...this.config,
...options,
header: mergedHeader
};
// 2. 拼接 baseURL如果 url 不是完整 HTTP 地址)
if (config.baseURL && config.url && !config.url.startsWith('http')) {
config.url = config.baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, '');
}
// 3. 执行请求拦截器
try {
for (const fn of this.interceptors.request) {
config = fn(config) || config;
}
} catch (err) {
reject({ success: false, message: err.message || '请求拦截器异常', code: -2 });
return;
}
// 4. 发起请求
uni.request({
url: config.url,
method: (config.method || 'GET').toUpperCase(),
data: config.data,
header: config.header,
timeout: config.timeout,
success: (res) => {
// 5. 执行响应拦截器
try {
for (const fn of this.interceptors.response) {
res = fn(res) || res;
}
} catch (err) {
reject({ success: false, message: err.message || '响应拦截器异常', code: -3, raw: res });
return;
}
// 6. 判断成功/失败
if (this.isSuccess(res)) {
resolve({
success: true,
data: this.extractData(res),
body: res.data,
raw: res
});
} else {
reject({
success: false,
message: this.extractError(res),
code: res.data?.code ?? res.data?.status ?? res.statusCode,
raw: res
});
}
},
fail: (err) => {
reject({
success: false,
message: err.errMsg || '网络请求失败,请检查网络',
code: -1,
raw: err
});
}
});
});
}
/**
* GET 请求快捷方法
* @param {string} url
* @param {Object} data
* @param {Object} options
*/
get(url, data, options = {}) {
return this.request({ url, data, method: 'GET', ...options });
}
/**
* POST 请求快捷方法
* @param {string} url
* @param {Object} data
* @param {Object} options
*/
post(url, data, options = {}) {
return this.request({ url, data, method: 'POST', ...options });
}
/**
* PUT 请求快捷方法
*/
put(url, data, options = {}) {
return this.request({ url, data, method: 'PUT', ...options });
}
/**
* DELETE 请求快捷方法
*/
delete(url, data, options = {}) {
return this.request({ url, data, method: 'DELETE', ...options });
}
}
// ============================================================
// 创建全局默认实例
// ============================================================
const http = new Request();
// ============================================================
// 导出
// ============================================================
export default http;
export { Request };

257
scratch/test_request.mjs Normal file
View File

@ -0,0 +1,257 @@
/**
* uni.request 统一封装
*
* 设计目标
* 1. 自动兼容项目中所有 API 的成功状态判断code === 200 / code === 1 / status === 200
* 2. 支持自定义 header超时baseURL
* 3. Promise 调用简单
* 4. 支持请求/响应拦截器便于后续统一注入 token处理错误等
* 5. 数据提取自动降级优先取 res.data.data不存在则取 res.data
*/
class Request {
/**
* @param {Object} config - 全局默认配置
* @param {string} config.baseURL - 基础 URL
* @param {number} config.timeout - 超时时间ms默认 30000
* @param {Object} config.header - 默认请求头
*/
constructor(config = {}) {
this.config = {
baseURL: '',
timeout: 30000,
header: {
'Content-Type': 'application/json'
},
...config
};
this.interceptors = {
request: [],
response: []
};
}
/**
* 添加请求拦截器
* @param {Function} fn - (config) => config
*/
addRequestInterceptor(fn) {
if (typeof fn === 'function') {
this.interceptors.request.push(fn);
}
}
/**
* 添加响应拦截器
* @param {Function} fn - (response) => response
*/
addResponseInterceptor(fn) {
if (typeof fn === 'function') {
this.interceptors.response.push(fn);
}
}
/**
* 判断响应是否成功
*
* 兼容项目中实际存在的 4 种响应格式
* 1. CMS API: res.data.code === 200
* 2. Auth API: res.statusCode === 200 && (res.data.status === 200 || res.data.code === 1)
* 3. Hdk v2 API: res.data.code === 1
* 4. 兜底: res.statusCode === 200无业务状态码时
*
* @param {Object} res - uni.request success 回调参数
* @returns {boolean}
*/
isSuccess(res) {
// HTTP 层已失败(如 404/500
if (res.statusCode >= 400) {
return false;
}
const data = res.data;
if (!data || typeof data !== 'object') {
// 无响应体时,以 HTTP 状态码为准
return res.statusCode === 200;
}
// 提取业务状态码(兼容 code / status 两种字段名)
const code = data.code !== undefined ? Number(data.code)
: data.status !== undefined ? Number(data.status)
: null;
if (code !== null && !isNaN(code)) {
return code === 200 || code === 1;
}
// 无业务状态码时,以 HTTP 状态码为准
return res.statusCode === 200;
}
/**
* 从响应中提取业务数据
*
* 降级策略
* - 优先取 res.data.dataCMS 标准格式
* - 不存在则取 res.data 本身直接返回格式
* - 空响应返回 null
*
* @param {Object} res - uni.request success 回调参数
* @returns {any}
*/
extractData(res) {
const data = res.data;
if (!data || typeof data !== 'object') {
return data;
}
return data.data !== undefined ? data.data : data;
}
/**
* 从响应中提取错误信息
* @param {Object} res - uni.request success 回调参数
* @returns {string}
*/
extractError(res) {
const data = res.data;
if (!data || typeof data !== 'object') {
return `请求失败 (HTTP ${res.statusCode})`;
}
return data.msg || data.message || data.error || `请求失败 (code: ${data.code || data.status})`;
}
/**
* 核心请求方法
*
* @param {Object} options - 请求配置
* @param {string} options.url - 请求地址可相对 baseURL
* @param {string} options.method - 请求方法GET/POST/PUT/DELETE默认 GET
* @param {Object} options.data - 请求数据
* @param {Object} options.header - 自定义请求头会合并覆盖默认 header大小写不敏感
* @param {number} options.timeout - 本次请求单独设置的超时时间
* @returns {Promise<{success: true, data: any, body: any, raw: Object}>}
*/
request(options = {}) {
return new Promise((resolve, reject) => {
// 1. 合并配置:默认 → 实例配置 → 单次请求配置
// header 合并需大小写不敏感去重(如 Content-Type 与 content-type 是同一头)
const mergedHeader = {};
for (const [k, v] of Object.entries(this.config.header)) {
mergedHeader[k.toLowerCase()] = v;
}
for (const [k, v] of Object.entries(options.header || {})) {
mergedHeader[k.toLowerCase()] = v;
}
let config = {
...this.config,
...options,
header: mergedHeader
};
// 2. 拼接 baseURL如果 url 不是完整 HTTP 地址)
if (config.baseURL && config.url && !config.url.startsWith('http')) {
config.url = config.baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, '');
}
// 3. 执行请求拦截器
try {
for (const fn of this.interceptors.request) {
config = fn(config) || config;
}
} catch (err) {
reject({ success: false, message: err.message || '请求拦截器异常', code: -2 });
return;
}
// 4. 发起请求
uni.request({
url: config.url,
method: (config.method || 'GET').toUpperCase(),
data: config.data,
header: config.header,
timeout: config.timeout,
success: (res) => {
// 5. 执行响应拦截器
try {
for (const fn of this.interceptors.response) {
res = fn(res) || res;
}
} catch (err) {
reject({ success: false, message: err.message || '响应拦截器异常', code: -3, raw: res });
return;
}
// 6. 判断成功/失败
if (this.isSuccess(res)) {
resolve({
success: true,
data: this.extractData(res),
body: res.data,
raw: res
});
} else {
reject({
success: false,
message: this.extractError(res),
code: res.data?.code ?? res.data?.status ?? res.statusCode,
raw: res
});
}
},
fail: (err) => {
reject({
success: false,
message: err.errMsg || '网络请求失败,请检查网络',
code: -1,
raw: err
});
}
});
});
}
/**
* GET 请求快捷方法
* @param {string} url
* @param {Object} data
* @param {Object} options
*/
get(url, data, options = {}) {
return this.request({ url, data, method: 'GET', ...options });
}
/**
* POST 请求快捷方法
* @param {string} url
* @param {Object} data
* @param {Object} options
*/
post(url, data, options = {}) {
return this.request({ url, data, method: 'POST', ...options });
}
/**
* PUT 请求快捷方法
*/
put(url, data, options = {}) {
return this.request({ url, data, method: 'PUT', ...options });
}
/**
* DELETE 请求快捷方法
*/
delete(url, data, options = {}) {
return this.request({ url, data, method: 'DELETE', ...options });
}
}
// ============================================================
// 创建全局默认实例
// ============================================================
const http = new Request();
// ============================================================
// 导出
// ============================================================
export default http;
export { Request };

346
scratch/url-query.js Normal file
View File

@ -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 APIH5 / 现代环境)
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

64
store/index.js Normal file
View File

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

150
test.vue Normal file
View File

@ -0,0 +1,150 @@
<template>
<view class="test-page">
<view class="desc-card">
<text class="title">IntersectionObserver 仅首次触发演示</text>
<text class="subtitle">向下滚动观察各模块首次进入视口时的淡入效果</text>
</view>
<view class="section-title">场景仅首次触发 视口内淡入动画</view>
<view
class="fade-box"
v-for="(item, index) in fadeList"
:key="index"
:id="`fade-${index}`"
:class="{ 'fade-in': item.visible }"
>
<text class="box-text">模块 {{ index + 1 }}</text>
<text class="box-status">{{ item.triggered ? '✅ 已触发(不再重复)' : '⏳ 等待首次进入视口...' }}</text>
</view>
<view class="footer-space"></view>
</view>
</template>
<script>
export default {
data() {
return {
fadeList: Array.from({ length: 10 }, () => ({
visible: false,
triggered: false
})),
// observer
observers: []
}
},
onReady() {
this.initFadeObserver();
},
onUnload() {
// observer
this.observers.forEach(obs => obs.disconnect());
},
methods: {
initFadeObserver() {
this.fadeList.forEach((_, index) => {
// IntersectionObserver
// observe()
const observer = uni.createIntersectionObserver(this, {
thresholds: [0]
});
observer.relativeToViewport();
observer.observe(`#fade-${index}`, (result) => {
const item = this.fadeList[index];
//
if (result.intersectionRatio > 0 && !item.triggered) {
console.log(`模块 ${index + 1} 首次进入视口`);
this.$set(this.fadeList, index, {
visible: true,
triggered: true
});
// observer
observer.disconnect();
}
});
// 便 onUnload
this.observers.push(observer);
});
}
}
}
</script>
<style scoped>
.test-page {
background-color: #f5f6f8;
padding: 30rpx;
}
.desc-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 40rpx;
}
.title {
color: #ffffff;
font-size: 40rpx;
font-weight: bold;
display: block;
}
.subtitle {
color: rgba(255, 255, 255, 0.85);
font-size: 26rpx;
margin-top: 16rpx;
display: block;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin: 40rpx 0 20rpx;
padding-left: 20rpx;
border-left: 8rpx solid #ff416c;
}
.fade-box {
height: 240rpx;
background: #ffffff;
border-radius: 20rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
opacity: 0;
transform: translateY(60rpx);
transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.fade-box.fade-in {
opacity: 1;
transform: translateY(0);
}
.box-text {
font-size: 36rpx;
color: #333;
font-weight: bold;
}
.box-status {
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
}
.footer-space {
height: 100rpx;
}
</style>

View File

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

271
utils/env.js Normal file
View File

@ -0,0 +1,271 @@
/**
* ============================================================
* 环境检测器 - uni-app 适配版
* 支持H5微信小程序支付宝小程序百度小程序抖音小程序App
* ============================================================
*
* 原始版本基于 navigator.userAgentH5 专用
* 本版本通过 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
};

5
utils/index.js Normal file
View File

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