mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-06 17:09:01 +08:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d72f02f7cf | ||
|
|
7b2b045fd4 | ||
|
|
e1a48d6d11 | ||
|
|
1411f15679 | ||
|
|
af7bd78614 | ||
|
|
0170c92f9a | ||
|
|
5e59f2cd3c | ||
|
|
6b59ae3d6e | ||
|
|
b589c551bf | ||
|
|
b8a60a9369 | ||
|
|
1032b6e34d | ||
|
|
0a47f84c32 | ||
|
|
2d4a316129 | ||
|
|
b1d3f56f10 | ||
|
|
87477bc32b | ||
|
|
6c13dc1b65 | ||
|
|
215754a6de | ||
|
|
c15e954549 | ||
|
|
755f574e8e | ||
|
|
986625c516 | ||
|
|
a41936dc00 | ||
|
|
eba79a78e8 | ||
|
|
a700a3d211 | ||
|
|
b603f8c451 | ||
|
|
b773ada7a8 | ||
|
|
40d42f001d | ||
|
|
a108bb4d13 | ||
|
|
79433dd766 | ||
|
|
fb10038547 | ||
|
|
c6d58f28b7 | ||
|
|
a32f6d2313 | ||
|
|
9cf9957e5b | ||
|
|
cc9d5535cf | ||
|
|
74d186ff36 | ||
|
|
a3193c3b68 | ||
|
|
e858741bb8 | ||
|
|
0bffe81c6b | ||
|
|
2107e3f03a | ||
|
|
27034902fe | ||
|
|
42e9b7c15f | ||
|
|
30ce901137 | ||
|
|
7cc605bed1 | ||
|
|
2ad8a7f5a6 | ||
|
|
e30ff03b10 | ||
|
|
ba4e9126e8 | ||
|
|
43bf0773bc | ||
|
|
f6d4fac35e | ||
|
|
ef3401a9e4 | ||
|
|
8f540224bb | ||
|
|
c12c76f6f5 | ||
|
|
4fdd266dec | ||
|
|
9d18d09cab | ||
|
|
4a194ee0a1 | ||
|
|
69b8395686 | ||
|
|
907ad2d02c | ||
|
|
651d1106da | ||
|
|
4b1a09cbc3 | ||
|
|
ee6f66cac3 | ||
|
|
f51fcad9c2 | ||
|
|
359d1ad24c | ||
|
|
7d3daf2f02 | ||
|
|
eb40916cf7 | ||
|
|
370c63afcd | ||
|
|
94e846af6c | ||
|
|
a6ba2b3dbe | ||
|
|
6754a54f1b | ||
|
|
7626f944ad | ||
|
|
27134526a0 | ||
|
|
4bb4bbdc6b | ||
|
|
239855e53b | ||
|
|
8ed2997fcf | ||
|
|
3ec61f41bc | ||
|
|
38defd1201 | ||
|
|
7566076fa4 | ||
|
|
41e8cff749 | ||
|
|
f727e01ef0 | ||
|
|
02b5f246a6 | ||
|
|
349ffa4f34 | ||
|
|
4d0b0fb5e4 | ||
|
|
fc3cad18e6 | ||
|
|
23e3825366 | ||
|
|
686aed848d | ||
|
|
4e2fcbc1fe | ||
|
|
e14481ce6b | ||
|
|
6198a6d244 | ||
|
|
90ee7d5b1d | ||
|
|
b1743cefb0 | ||
|
|
2965800d87 | ||
|
|
3a44b7243d | ||
|
|
5f293d929d | ||
|
|
6654c1de51 | ||
|
|
dc2bef455a | ||
|
|
c290c7a329 | ||
|
|
d5663cfb4d | ||
|
|
b7cfe13ffd | ||
|
|
134a8b19e4 | ||
|
|
137232290b | ||
|
|
87743cb77d | ||
|
|
7939998a30 | ||
|
|
1fcc016ccd | ||
|
|
3219f6d576 | ||
|
|
83da8f3003 | ||
|
|
c858c82950 | ||
|
|
f9c7610976 | ||
|
|
919af44fda | ||
|
|
db7cfcea85 | ||
|
|
e95aed8e38 | ||
|
|
24454e2546 | ||
|
|
0ee03d9fcf | ||
|
|
c93862066b | ||
|
|
46dbeb77c8 | ||
|
|
8bcc9de352 | ||
|
|
bac8e3a327 | ||
|
|
29a20afe93 | ||
|
|
0ef3eb9bf7 | ||
|
|
ef6f572311 | ||
|
|
c1fce859ce | ||
|
|
2cd7b7b63f | ||
|
|
75991db6f6 | ||
|
|
f894b5b1fc | ||
|
|
7ae6967b1e | ||
|
|
de94f212c3 | ||
|
|
d166d5943c | ||
|
|
c0e5d75cc6 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,13 +1,4 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
node_modules/
|
||||
|
||||
#Intellij idea
|
||||
unpackage/
|
||||
.DS_Store
|
||||
.idea/
|
||||
/idea/
|
||||
.vscode/
|
||||
/unpackage/
|
||||
.hbuilderx/launch.json
|
||||
.project.config.json
|
||||
.project.private.config.json
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
|
||||
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
|
||||
"version": "0.0",
|
||||
"configurations": [{
|
||||
"app-plus" :
|
||||
{
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"default" :
|
||||
{
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"h5" :
|
||||
{
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"mp-weixin" :
|
||||
{
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"type" : "uniCloud"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
603
App.vue
603
App.vue
@@ -1,253 +1,350 @@
|
||||
<script>
|
||||
/**
|
||||
* vuex管理登录状态,具体可以参考官方登录模板示例
|
||||
*/
|
||||
import config from "@/config/config";
|
||||
import {
|
||||
getClipboardData
|
||||
} from "@/js_sdk/h5-copy/h5-copy.js";
|
||||
import APPUpdate from "@/plugins/APPUpdate";
|
||||
import storage from "@/utils/storage";
|
||||
import {
|
||||
mapMutations
|
||||
} from "vuex";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 路由监听并删除路由
|
||||
* https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html
|
||||
* */
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.onAppRoute((res) => {
|
||||
|
||||
})
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 监听返回
|
||||
*/
|
||||
onBackPress(e) {
|
||||
console.log("onBackPress-APP", e);
|
||||
if (e.from == "backbutton") {
|
||||
let routes = getCurrentPages();
|
||||
let curRoute = routes[routes.length - 1].options;
|
||||
routes.forEach((item) => {
|
||||
if (
|
||||
item.route == "pages/tabbar/cart/cartList" ||
|
||||
item.route.indexOf("pages/product/goods") != -1
|
||||
) {
|
||||
uni.redirectTo({
|
||||
url: item.route,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (curRoute.addId) {
|
||||
uni.reLaunch({
|
||||
url: "/pages/tabbar/cart/cartList",
|
||||
});
|
||||
} else {
|
||||
uni.navigateBack();
|
||||
}
|
||||
return true; //阻止默认返回行为
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["login"]),
|
||||
},
|
||||
onLaunch: function() {
|
||||
// #ifdef APP-PLUS
|
||||
this.checkArguments(); // 检测启动参数
|
||||
APPUpdate();
|
||||
|
||||
// 重点是以下: 一定要监听后台恢复 !一定要
|
||||
plus.globalEvent.addEventListener("newintent", (e) => {
|
||||
this.checkArguments(); // 检测启动参数
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
this.applyUpdateWeChat();
|
||||
// #endif
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// #ifndef H5
|
||||
if(this.config.enableGetClipboard){
|
||||
this.getClipboard();
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
|
||||
if (storage.getShow()) {
|
||||
if (uni.getSystemInfoSync().platform == 'ios') {
|
||||
this.$u.route("/pages/tabbar/screen/fullScreen");
|
||||
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 微信小程序版本提交更新版本 解决缓存问题
|
||||
*/
|
||||
applyUpdateWeChat() {
|
||||
const updateManager = uni.getUpdateManager();
|
||||
|
||||
updateManager.onCheckForUpdate(function(res) {
|
||||
// 请求完新版本信息的回调
|
||||
});
|
||||
|
||||
updateManager.onUpdateReady(function(res) {
|
||||
uni.showModal({
|
||||
title: "更新提示",
|
||||
content: "发现新版本,是否重启应用?",
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
|
||||
updateManager.applyUpdate();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
updateManager.onUpdateFailed(function(res) {
|
||||
// 新的版本下载失败
|
||||
});
|
||||
},
|
||||
|
||||
// TODO 开屏广告 后续优化添加
|
||||
launch() {
|
||||
try {
|
||||
// 获取本地存储中launchFlag标识 开屏广告
|
||||
const value = uni.getStorageSync("launchFlag");
|
||||
if (!value) {
|
||||
// this.$u.route("/pages/index/agreement");
|
||||
} else {
|
||||
//app启动时打开启动广告页
|
||||
var w = plus.webview.open(
|
||||
"/hybrid/html/advertise/advertise.html",
|
||||
"本地地址", {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zindex: 999,
|
||||
},
|
||||
"fade-in",
|
||||
500
|
||||
);
|
||||
//设置定时器,4s后关闭启动广告页
|
||||
setTimeout(function() {
|
||||
plus.webview.close(w);
|
||||
APPUpdate();
|
||||
}, 3000);
|
||||
}
|
||||
} catch (e) {
|
||||
// error
|
||||
uni.setStorage({
|
||||
key: "launchFlag",
|
||||
data: true,
|
||||
success: function() {
|
||||
console.log("error时存储launchFlag");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取粘贴板数据
|
||||
*/
|
||||
async getClipboard() {
|
||||
let res = await getClipboardData();
|
||||
|
||||
/**
|
||||
* 解析粘贴板数据
|
||||
*/
|
||||
|
||||
if (res.indexOf(config.shareLink) != -1 && (res != this.$store.state.shareLink)) {
|
||||
this.$store.state.shareLink = res
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "检测到一个分享链接是否跳转?",
|
||||
confirmText: "跳转",
|
||||
success: function(callback) {
|
||||
if (callback.confirm) {
|
||||
const path = res.split(config.shareLink)[1];
|
||||
if (path.indexOf("tabbar") != -1) {
|
||||
uni.switchTab({
|
||||
url: path,
|
||||
});
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* h5中打开app获取跳转app的链接并跳转
|
||||
*/
|
||||
checkArguments() {
|
||||
// #ifdef APP-PLUS
|
||||
setTimeout(() => {
|
||||
const args = plus.runtime.arguments;
|
||||
if (args) {
|
||||
const argsStr = decodeURIComponent(args);
|
||||
const path = argsStr.split("//")[1];
|
||||
if (path.indexOf("tabbar") != -1) {
|
||||
uni.switchTab({
|
||||
url: `/${path}`,
|
||||
});
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/${path}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "uview-ui/index.scss";
|
||||
|
||||
// -------适配底部安全区 苹果x系列刘海屏
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
.mp-iphonex-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: content-box;
|
||||
height: auto !important;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
body {
|
||||
background-color: $bg-color;
|
||||
}
|
||||
|
||||
/************************ */
|
||||
.w200 {
|
||||
width: 200rpx !important;
|
||||
}
|
||||
|
||||
.flex1 {
|
||||
flex: 1; //必须父级设置flex
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import config from '@/config/config'
|
||||
import { getClipboardData } from '@/js_sdk/h5-copy/h5-copy.js'
|
||||
import APPUpdate from '@/plugins/APPUpdate'
|
||||
import storage from '@/utils/storage'
|
||||
import { getThemeSetting, getCategoryPageSetting } from '@/api/common.js'
|
||||
import {
|
||||
applyTabBarStyle,
|
||||
cacheTheme,
|
||||
loadCachedTheme,
|
||||
normalizeTheme,
|
||||
} from '@/utils/theme'
|
||||
import {
|
||||
cacheCategoryPageSetting,
|
||||
loadCachedCategoryPageSetting,
|
||||
normalizeCategoryPageSetting,
|
||||
} from '@/utils/categoryPageSetting'
|
||||
import { onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { handleAppLaunchScene } from '@/utils/distributionBind.js'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.onAppRoute((res) => {
|
||||
applyTabBarStyle(store.state.theme && store.state.theme.mainColor, res && res.path)
|
||||
})
|
||||
// #endif
|
||||
|
||||
onLaunch((val) => {
|
||||
initTheme()
|
||||
initCategoryPageSetting()
|
||||
handleAppLaunchScene(val)
|
||||
if (val?.query?.inviter) {
|
||||
storage.setInviter(val.query.inviter)
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.globalEvent.addEventListener('newintent', () => {
|
||||
checkArguments()
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
applyUpdateWeChat()
|
||||
// #endif
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
applyTabBarStyle(store.state.theme && store.state.theme.mainColor)
|
||||
|
||||
// #ifndef H5
|
||||
if (config.enableGetClipboard) {
|
||||
getClipboard()
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
if (storage.getShow()) {
|
||||
if (uni.getSystemInfoSync().platform == 'ios') {
|
||||
uni.navigateTo({ url: '/pages/tabbar/screen/fullScreen' })
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
function initTheme() {
|
||||
const cached = loadCachedTheme()
|
||||
if (cached) {
|
||||
store.commit('SET_THEME', cached)
|
||||
applyTabBarStyle(cached.mainColor)
|
||||
}
|
||||
getThemeSetting()
|
||||
.then((res) => {
|
||||
if (res.data?.success && res.data.result?.settingValue) {
|
||||
try {
|
||||
const raw = JSON.parse(res.data.result.settingValue)
|
||||
const theme = normalizeTheme(raw)
|
||||
store.commit('SET_THEME', theme)
|
||||
cacheTheme(theme)
|
||||
applyTabBarStyle(theme.mainColor)
|
||||
} catch (e) {
|
||||
// 主题配置解析失败时使用缓存
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function initCategoryPageSetting() {
|
||||
const cached = loadCachedCategoryPageSetting()
|
||||
if (cached) {
|
||||
store.commit('SET_CATEGORY_PAGE_SETTING', cached)
|
||||
}
|
||||
getCategoryPageSetting()
|
||||
.then((res) => {
|
||||
if (res.data?.success) {
|
||||
const setting = normalizeCategoryPageSetting(res.data.result)
|
||||
store.commit('SET_CATEGORY_PAGE_SETTING', setting)
|
||||
cacheCategoryPageSetting(setting)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function applyUpdateWeChat() {
|
||||
const updateManager = uni.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate(() => {})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
uni.showModal({
|
||||
title: '更新提示',
|
||||
content: '发现新版本,是否重启应用?',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {})
|
||||
}
|
||||
|
||||
function launch() {
|
||||
try {
|
||||
const value = uni.getStorageSync('launchFlag')
|
||||
if (!value) {
|
||||
// uni.navigateTo({ url: '/pages/index/agreement' })
|
||||
} else {
|
||||
const w = plus.webview.open(
|
||||
'/hybrid/html/advertise/advertise.html',
|
||||
'本地地址',
|
||||
{
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zindex: 999,
|
||||
},
|
||||
'fade-in',
|
||||
500,
|
||||
)
|
||||
setTimeout(() => {
|
||||
plus.webview.close(w)
|
||||
APPUpdate()
|
||||
}, 3000)
|
||||
}
|
||||
} catch (e) {
|
||||
uni.setStorage({
|
||||
key: 'launchFlag',
|
||||
data: true,
|
||||
success() {
|
||||
console.log('error时存储launchFlag')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function getClipboard() {
|
||||
const res = await getClipboardData()
|
||||
if (res.indexOf(config.shareLink) != -1 && res != store.state.shareLink) {
|
||||
store.state.shareLink = res
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '检测到一个分享链接是否跳转?',
|
||||
confirmText: '跳转',
|
||||
success(callback) {
|
||||
if (callback.confirm) {
|
||||
const path = res.split(config.shareLink)[1]
|
||||
if (path.indexOf('tabbar') != -1) {
|
||||
uni.switchTab({ url: path })
|
||||
} else {
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function checkArguments() {
|
||||
// #ifdef APP-PLUS
|
||||
setTimeout(() => {
|
||||
const args = plus.runtime.arguments
|
||||
if (args) {
|
||||
const argsStr = decodeURIComponent(args)
|
||||
const path = argsStr.split('//')[1]
|
||||
if (path.indexOf('tabbar') != -1) {
|
||||
uni.switchTab({ url: `/${path}` })
|
||||
} else {
|
||||
uni.navigateTo({ url: `/${path}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "uview-plus/index.scss";
|
||||
@import "@/styles/uview-form-fix.scss";
|
||||
|
||||
// -------适配底部安全区 苹果x系列刘海屏
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
.mp-iphonex-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: content-box;
|
||||
height: auto !important;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
body {
|
||||
background-color: $bg-color;
|
||||
}
|
||||
|
||||
/************************ */
|
||||
.w200 {
|
||||
width: 200rpx !important;
|
||||
}
|
||||
|
||||
.flex1 {
|
||||
flex: 1; //必须父级设置flex
|
||||
}
|
||||
|
||||
.light-color {
|
||||
color: $light-color;
|
||||
}
|
||||
.main-color {
|
||||
color: $main-color !important;
|
||||
}
|
||||
.bg-light-color {
|
||||
background-color: $light-color !important;
|
||||
}
|
||||
.bg-main-color {
|
||||
background-color: $main-color !important;
|
||||
}
|
||||
|
||||
// 渐变主题颜色
|
||||
.bg-linear-gradient {
|
||||
background-image: linear-gradient(25deg, var(--theme-primary, #fa123b), var(--theme-light, #ff6b35), var(--theme-aider, #ff9f28), #ffcc03);
|
||||
}
|
||||
.uni-tabbar .uni-tabbar__icon {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: $light-color;
|
||||
color: #ffffff;
|
||||
border-width: 0px;
|
||||
border: none;
|
||||
border-radius: 44px;
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.button-hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: rgba(242, 242, 242, 1);
|
||||
color: #333333;
|
||||
height: 28px;
|
||||
border-radius: 28px;
|
||||
box-shadow: none;
|
||||
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.c-content {
|
||||
background-color: #ffffff;
|
||||
// margin-top: 30rpx;
|
||||
border-top: 1px solid $border-color-light;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.border-top {
|
||||
border-top: 1px solid $border-color-light;
|
||||
}
|
||||
.describe {
|
||||
color: $font-color-light;
|
||||
font-size: $font-sm;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-a-c{
|
||||
align-items: center;
|
||||
}
|
||||
.flex-j-sb{
|
||||
justify-content: space-between;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wes {
|
||||
/* 多出部分用省略号表示 , 用于一行 */
|
||||
overflow: hidden;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wes-2 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wes-3 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
866
LICENSE
866
LICENSE
@@ -1,661 +1,205 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
Apache License, Version 2.0
|
||||
|
||||
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
140
README.en.md
Normal file
140
README.en.md
Normal file
@@ -0,0 +1,140 @@
|
||||
<div align="center">
|
||||
<img src="./icon.png" width="80" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) - Mobile Client (uni-app)</h1>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://pickmall.cn">🏠 Official Website</a> ·
|
||||
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 Product Documentation</a> ·
|
||||
<a href="#2-online-demo">📱 Online Demo</a> ·
|
||||
<a href="https://github.com/lilishop/lilishop">💻 GitHub</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="./README.md">简体中文</a> | <b>English</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
> ⭐ **If you like this project, please click `Star` in the top-right corner to support us! Your Star is our greatest motivation for continuous open-source iteration!**
|
||||
|
||||
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
||||
|
||||
### ✨ Why Choose Lilishop
|
||||
- 🟢 **True Apache-2.0 License**: Free for commercial use and private secondary development without licensing concerns.
|
||||
- 🚀 **Modern Vue 3 Stack**: Built on `uni-app + Vue 3 + uview-plus 3.x`, leaving legacy Vue 2 templates behind.
|
||||
- 🧩 **Production-Ready Completeness**: Not a static UI template—fully integrated with B2B2C multi-vendor workflows, visual storefront decoration, IM customer service, flash sales, group buying, affiliate distribution, and live streaming.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This repository (`lilishop-uniapp`) is the **Mobile Buyer Client** of Lilishop, supporting **H5, WeChat Mini Program, Android, and iOS Apps** from a single codebase powered by **Vue 3** and **uni-app**.
|
||||
|
||||
- **Official Website**: <https://pickmall.cn>
|
||||
- **Documentation**: [Product Documentation](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
|
||||
- **Gitee Repository**: <https://gitee.com/beijing_hongye_huicheng>
|
||||
- **GitHub Main Repository**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
---
|
||||
|
||||
## 2. Online Demo
|
||||
|
||||
> **Note**: The verification code for the demo environment is `111111`.
|
||||
|
||||
| WeChat Mini Program | Mobile H5 ([Visit Directly](https://m-b2b2c.pickmall.cn)) |
|
||||
| :---: | :---: |
|
||||
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="WeChat Mini Program QR Code" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="Mobile H5 QR Code" /> |
|
||||
| Scan with WeChat | Scan or visit <https://m-b2b2c.pickmall.cn> |
|
||||
|
||||
- **PC Mall**: <https://pc-b2b2c.pickmall.cn>
|
||||
- **Admin Panel**: <https://admin-b2b2c.pickmall.cn> (`admin` / `123456`)
|
||||
- **Store Panel**: <https://store-b2b2c.pickmall.cn> (`13011111111` / `111111`)
|
||||
|
||||
### Mobile UI Preview
|
||||
|
||||
| Home | Product List | Product Detail | Checkout | User Center |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image.png" width="160" alt="Home" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image3.png" width="160" alt="Product List" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image4.png" width="160" alt="Product Detail" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image5.png" width="160" alt="Checkout" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image2.png" width="160" alt="User Center" /> |
|
||||
|
||||
---
|
||||
|
||||
## 3. Core Features & Tech Stack
|
||||
|
||||
### Core Features
|
||||
- **Cross-Platform**: One codebase runs on H5, WeChat Mini Program, Android, and iOS.
|
||||
- **Multi-Vendor B2B2C**: Supports merchant onboarding and store-level operations.
|
||||
- **Visual Storefront Decoration**: Dynamic home page & floor layouts (`pages/floor`) and customizable theme colors.
|
||||
- **Live Streaming & IM**: Integrated with Tencent Cloud IM / MQTT and HLS video streaming.
|
||||
- **Full E-Commerce Workflow**: Product browsing, cart, promotions (flash sale, group buy, coupons, points, bargain, affiliate), order fulfillment, after-sales, and member center.
|
||||
|
||||
### Mobile Tech Stack
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| Core Framework | `uni-app` + `Vue 3` | Cross-platform framework with Vue 3 support |
|
||||
| State Management | `Vuex 4` | Global auth, cart, and user state management |
|
||||
| UI Library | `uview-plus 3.x` | Vue 3 compatible uni-app multi-platform UI library |
|
||||
| Messaging & IM | `mqtt 5.x` / `@tencentcloud/chat` | Real-time customer service and notification push |
|
||||
| Video Streaming | `hls.js` | Live streaming playback support |
|
||||
| CSS Preprocessor | `Sass (SCSS)` | Global theme variables (`uni.scss`) and styling |
|
||||
|
||||
---
|
||||
|
||||
## 4. Directory Structure
|
||||
|
||||
```text
|
||||
lilishop-uniapp
|
||||
├── api/ # API module definitions (member, goods, order, promotion, IM, etc.)
|
||||
├── components/ # Shared business and base components
|
||||
├── config/ # Global configuration
|
||||
│ ├── api.js # API base URLs (buyer / im / mqtt)
|
||||
│ └── config.js # Mall name, Mini Program AppID, AMap Key, theme colors, etc.
|
||||
├── hybrid/ # Local HTML assets for App WebView
|
||||
├── js_sdk/ # Third-party and uni-app SDKs
|
||||
├── lib/ # Base utility libraries
|
||||
├── pages/ # Pages and sub-packages
|
||||
│ ├── tabbar/ # Main tabbar pages (Home, Category, Cart, Mine)
|
||||
│ ├── passport/ # Auth sub-package (Login, Register, Reset Password)
|
||||
│ ├── product/ # Product detail, list, reviews, store homepage
|
||||
│ ├── cart/ # Shopping cart and checkout
|
||||
│ ├── order/ # Order list, detail, after-sales, logistics, invoice
|
||||
│ ├── promotion/ # Flash sale, group buy, bargain, points mall, distribution, live
|
||||
│ ├── mine/ # User center, wallet, points, favorites, addresses
|
||||
│ ├── floor/ # Visual decoration floor components and topic pages
|
||||
│ └── navigation/ # Navigation and search pages
|
||||
├── patches/ # patch-package patches for dependencies
|
||||
├── scripts/ # Build and postinstall runtime patch scripts
|
||||
├── static/ # Static images and fonts
|
||||
├── store/ # Vuex store modules
|
||||
├── styles/ # Global stylesheets
|
||||
├── uni_modules/ # uni_modules plugins (including uview-plus)
|
||||
├── utils/ # Request interceptors, encryption, validators, storage helpers
|
||||
├── App.vue # Root application component
|
||||
├── main.js # Application entry point
|
||||
├── manifest.json # uni-app multi-platform configuration
|
||||
├── pages.json # Page routes, sub-packages, and navigation bar settings
|
||||
└── uni.scss # Global SCSS variables and theme color definitions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Related Repositories
|
||||
|
||||
| Repository | Description | Links |
|
||||
| :--- | :--- | :--- |
|
||||
| **`lilishop`** | Backend Services (Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
|
||||
| **`lilishop-ui`** | PC Mall / Seller / Admin / IM Web Frontend | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
|
||||
| **`lilishop-uniapp`** | Mobile Buyer Client (H5 / Mini Program / App) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
|
||||
| **`docker`** | Docker Compose, Images & SQL Init Scripts | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL Scripts](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
|
||||
|
||||
---
|
||||
|
||||
## 6. License
|
||||
|
||||
Lilishop is licensed under the [Apache-2.0](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
||||
323
README.md
323
README.md
@@ -1,217 +1,180 @@
|
||||
## Lilishop B2B2C 商城系统
|
||||
<div align="center">
|
||||
<img src="./icon.png" width="80" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) 开源商城系统 - 移动端 (uni-app)</h1>
|
||||
|
||||
### 商城介绍
|
||||
<p>
|
||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||
</p>
|
||||
|
||||
**官网**:https://pickmall.cn
|
||||
<p>
|
||||
<a href="https://pickmall.cn">🏠 官方网站</a> ·
|
||||
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 产品文档</a> ·
|
||||
<a href="#2-在线演示">📱 在线演示</a> ·
|
||||
<a href="#7-社区与支持">💬 微信交流群</a>
|
||||
</p>
|
||||
|
||||
**更多 README**:https://gitee.com/beijing_hongye_huicheng/lilishop/blob/master/README.md
|
||||
<p>
|
||||
<b>简体中文</b> | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
**演示站点**:https://m-b2b2c.pickmall.cn/ 账号:13011111111 验证码:111111
|
||||
> ⭐ **如果觉得项目不错,请点击右上角 `Star` 收藏支持!您的 Star 是我们持续开源迭代的最大动力!**
|
||||
|
||||
**商城 小程序/公众号/APP**:扫描二维码
|
||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||
|
||||

|
||||
### ✨ 为什么选择 Lilishop
|
||||
- 🟢 **真 Apache-2.0 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||
- 🚀 **Vue 3 全新栈**:基于 `uni-app + Vue 3 + uview-plus 3.x`,告别老旧 Vue 2 模板。
|
||||
- 🧩 **生产级完整度**:非静态切图模板,完整覆盖 B2B2C 多商户、可视化装修、IM 客服、秒杀拼团、分销直播全闭环。
|
||||
|
||||
## lilishop-uniapp如何在本地h5和微信小程序中运行
|
||||
https://www.bilibili.com/video/BV17K8EemEh2/
|
||||
---
|
||||
|
||||
## lilishop-uniapp如何打包h5到服务器
|
||||
https://www.bilibili.com/video/BV1CX87exE64/
|
||||
## 1. 项目简介
|
||||
|
||||
### 快速开始
|
||||
**Lilishop** 是一款功能完善的 B2B2C 多商户商城系统,采用前后端分离架构,全端代码开源。
|
||||
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署;前端覆盖 PC、H5、小程序和 APP,基于 **Vue 3** 和 **uni-app** 开发。
|
||||
|
||||
#### 阅读文档
|
||||
本仓库(`lilishop-uniapp`)为 **买家端移动端源码**,一套代码同时支持 **H5、微信小程序、Android / iOS APP**。
|
||||
|
||||
`uni-app` https://uniapp.dcloud.net.cn/api/
|
||||
- **官方网站**: <https://pickmall.cn>
|
||||
- **官方文档**: [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
|
||||
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng>
|
||||
- **GitHub 主仓库**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
`vue2` https://v2.cn.vuejs.org/
|
||||
---
|
||||
|
||||
`uView v1` https://v1.uviewui.com/
|
||||
## 2. 在线演示
|
||||
|
||||
#### 环境
|
||||
> **注意**: 演示站手机验证码统一为 `111111`。演示环境部署于 `master` 分支。
|
||||
|
||||
在开发之前保证本地安装了以下软件
|
||||
`hbulider` https://www.dcloud.io/hbuilderx.html
|
||||
| 微信小程序 | 移动端 H5([点击直接访问](https://m-b2b2c.pickmall.cn)) |
|
||||
| :---: | :---: |
|
||||
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="微信小程序体验码" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="移动端 H5 体验二维码" /> |
|
||||
| 微信扫码体验小程序 | 手机扫码或访问 <https://m-b2b2c.pickmall.cn> |
|
||||
|
||||
### 技术栈
|
||||
- **商城 PC 端**: <https://pc-b2b2c.pickmall.cn>
|
||||
- **平台管理端**: <https://admin-b2b2c.pickmall.cn>
|
||||
- 账号: `admin` / 密码: `123456`
|
||||
- **店铺管理端**: <https://store-b2b2c.pickmall.cn>
|
||||
- 账号: `13011111111` / 密码: `111111`
|
||||
|
||||
本项目技术栈为 `uni-app` + `scss` + `ES2015` + `uView` 提前学习和了解这些知识将帮助你更好地上手我们的项目。
|
||||
### 移动端界面预览
|
||||
|
||||
### 安装
|
||||
| 商城首页 | 商品列表 | 商品详情 | 填写订单 | 个人中心 |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image.png" width="160" alt="商城首页" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image3.png" width="160" alt="商品列表" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image4.png" width="160" alt="商品详情" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image5.png" width="160" alt="填写订单" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image2.png" width="160" alt="个人中心" /> |
|
||||
|
||||
1. 克隆项目到本地
|
||||
---
|
||||
|
||||
```shell
|
||||
git clone https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
|
||||
## 3. 核心特性与技术栈
|
||||
|
||||
### 核心特性
|
||||
- **全端覆盖**: 一套 `uni-app` 代码库支持 H5、微信小程序、Android / iOS APP,大幅降低多端开发与维护成本。
|
||||
- **商家入驻**: 支持多商家入驻,构建平台化 B2B2C 电商生态。
|
||||
- **可视化装修**: 支持首页及专题楼层(`pages/floor`)动态可视化配置与主题色切换。
|
||||
- **即时通讯与直播**: 集成腾讯云 IM / MQTT 消息推送及 HLS 视频流,支持在线客服与直播带货。
|
||||
- **完整电商闭环**: 涵盖商品浏览、购物车、多种促销(秒杀/拼团/优惠券/积分/砍价/分销)、订单履约、售后维权、会员中心等全流程。
|
||||
|
||||
### 移动端技术栈
|
||||
| 类别 | 技术选型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 核心框架 | `uni-app` + `Vue 3` | 跨端开发框架(支持 Vue 3 语法与渲染) |
|
||||
| 状态管理 | `Vuex 4` | 全局登录态、购物车与用户状态管理 |
|
||||
| UI 组件库 | `uview-plus 3.x` | 适配 Vue 3 的 uni-app 多端 UI 组件库 |
|
||||
| 即时通讯 | `mqtt 5.x` / `@tencentcloud/chat` | 客服消息与实时通知推送 |
|
||||
| 音视频流 | `hls.js` | 直播流播放支持 |
|
||||
| 样式预处理 | `Sass (SCSS)` | 全局主题变量(`uni.scss`)与组件样式定制 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 目录结构
|
||||
|
||||
```text
|
||||
lilishop-uniapp
|
||||
├── api/ # 各业务模块接口定义(会员、商品、订单、促销、IM 等)
|
||||
├── components/ # 公共业务与基础组件
|
||||
├── config/ # 全局配置目录
|
||||
│ ├── api.js # 接口域名配置(buyer / im / mqtt)
|
||||
│ └── config.js # 商城名称、小程序 AppID、高德 Key、主题色等全局参数
|
||||
├── hybrid/ # App 端本地 HTML 资源(如异常页、WebView 辅助页)
|
||||
├── js_sdk/ # 第三方及 uni-app 插件 SDK
|
||||
├── lib/ # 基础工具库封装
|
||||
├── pages/ # 页面与分包目录
|
||||
│ ├── tabbar/ # 底部导航主包页面(首页、分类、购物车、我的等)
|
||||
│ ├── passport/ # 登录、注册、找回密码分包
|
||||
│ ├── product/ # 商品详情、列表、评价、店铺主页分包
|
||||
│ ├── cart/ # 购物车与结算页分包
|
||||
│ ├── order/ # 订单列表、详情、售后、物流、发票分包
|
||||
│ ├── promotion/ # 秒杀、拼团、砍价、积分商城、分销、直播等营销分包
|
||||
│ ├── mine/ # 个人中心、钱包、积分、收藏、地址管理分包
|
||||
│ ├── floor/ # 可视化装修楼层组件与专题页
|
||||
│ └── navigation/ # 导航与搜索相关页面
|
||||
├── patches/ # patch-package 第三方依赖补丁
|
||||
├── scripts/ # 构建与依赖修补脚本(如 postinstall 补丁)
|
||||
├── static/ # 静态图片与字体资源
|
||||
├── store/ # Vuex 状态管理
|
||||
├── styles/ # 公共样式文件
|
||||
├── uni_modules/ # uni_modules 模块(含 uview-plus 等)
|
||||
├── utils/ # 请求拦截器、加密、校验、存储等工具函数
|
||||
├── App.vue # 应用入口组件
|
||||
├── main.js # 应用初始化入口
|
||||
├── manifest.json # uni-app 多端应用配置(AppID、权限、SDK 配置等)
|
||||
├── pages.json # 页面路由、分包加载与导航栏配置
|
||||
└── uni.scss # 全局 SCSS 变量与主题色定义
|
||||
```
|
||||
|
||||
2. 从`hbulider`中打开项目
|
||||
---
|
||||
|
||||
```shell
|
||||
直接将项目拖进hbulider中点击运行即可
|
||||
```
|
||||
## 5. 配套源码仓库
|
||||
|
||||
### 目录结构
|
||||
本仓库专注于买家端 **H5 / 小程序 / APP**(`lilishop-uniapp`)。完整系统由以下四个核心仓库组成:
|
||||
|
||||
```
|
||||
├── api // 接口
|
||||
├── components // 组件
|
||||
├── config // 配置文件
|
||||
├── CustomStartPage // 打包配置的开屏页
|
||||
├── CustomStoryboard // ios自定义storyboard启动页
|
||||
├── hybrid // 广告配置页
|
||||
├── js_sdk // 封装的js_sdk
|
||||
├── lib // 封装的部分方法
|
||||
├── pages // 页面
|
||||
├── plugins // 插件
|
||||
├── static // 静态资源
|
||||
├── store // vuex
|
||||
├── utils // 工具类
|
||||
├── uview-ui // uview
|
||||
├── App.vue // 入口页面
|
||||
├── main.js // 入口文件
|
||||
├── manifest.json // hbulider配置文件
|
||||
├── pages.json // 路由配置文件
|
||||
├── uni.scss // 全局样式
|
||||
└── vue.config.js // vue配置文件
|
||||
```
|
||||
| 仓库名称 | 职责说明 | 仓库地址 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`lilishop`** | 后端服务源码(Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
|
||||
| **`lilishop-ui`** | PC 商城 / 商家端 / 运营管理端 / IM Web 前端 | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
|
||||
| **`lilishop-uniapp`** | 买家端 H5 / 微信小程序 / Android & iOS APP | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
|
||||
| **`docker`** | 容器化部署编排、镜像与数据库脚本 | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL 脚本](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
|
||||
|
||||
### 运行
|
||||
---
|
||||
|
||||
#### 运行在 h5
|
||||
## 6. 开源与授权
|
||||
|
||||
在`hbulider`中点击`运行`->`运行到浏览器`->`选择自己的浏览器九号`
|
||||
本项目遵循 [Apache-2.0](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||
|
||||
#### 运行在微信小程序
|
||||
---
|
||||
|
||||
1.需要保证本地要有`微信开发者工具` https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
|
||||
## 7. 社区与支持
|
||||
|
||||
2.在`hbulider`中点击`运行`->`运行到小程序模拟器`->`微信开发者工具` 运行的时候需要配置小程序的`appid`,配置完成后即可运行
|
||||
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home),并参考 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md) 以便高效沟通。
|
||||
|
||||
#### 运行 APP
|
||||
- **[在线客服](https://work.weixin.qq.com/kfid/kfc4d8dc24a73c15f44)**
|
||||
- **微信交流群**:
|
||||
<img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/wechat.jpg" width="200" alt="微信群" />
|
||||
|
||||
##### 运行在安卓
|
||||
---
|
||||
|
||||
在`hbulider`中点击`运行`->`运行到手机或模拟器`->`选择运行到安卓APP基座`
|
||||
|
||||
##### 运行在 IOS
|
||||
|
||||
在`hbulider`中点击`运行`->`运行到手机或模拟器`->`选择运行到IOS基座`
|
||||
|
||||
|
||||
|
||||
|
||||
### 项目配置/开发
|
||||
|
||||
#### config配置
|
||||
在根目录`config`下的`config`设置中配置了一些默认的配置,可以根据自己的需求进行修改
|
||||
|
||||
```
|
||||
const name = "lilishop"; //全局商城name
|
||||
const schemeName = "lilishop"; //唤醒app需要的schemeName
|
||||
export default {
|
||||
name: name,
|
||||
schemeLink: `${schemeName}://`, //唤起app地址
|
||||
downloadLink: "https://pickmall.cn/download-page/index.html", //下载地址,下载app的地址
|
||||
shareLink: "https://m-b2b2c.pickmall.cn", //分享地址,也就是在h5中默认的复制地址
|
||||
appid: "", //小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
|
||||
aMapKey: "", //在高德中申请Web服务key
|
||||
scanAuthNavigation: ["https://m-b2b2c.pickmall.cn/"], //扫码认证跳转域名配置 会根据此处配置的路由进行跳转
|
||||
iosAppId: "id1564638363", //AppStore的应用地址id 具体在分享->拷贝链接中查看
|
||||
logo: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/4c864e133c2944efad1f7282ac8a3b9e.png", //logo地址
|
||||
customerServiceMobile: "13161366885", //客服电话
|
||||
customerServiceEmail: "lili@lili.com", //客服邮箱
|
||||
imWebSrc: "https://im.pickmall.cn", //IM地址
|
||||
baseWsUrl: "wss://im-api.pickmall.cn/lili/webSocket", // IM WS 地址
|
||||
enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面
|
||||
enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app
|
||||
/**
|
||||
* 如需更换主题请修改此处以及uni.scss中的全局颜色
|
||||
*/
|
||||
mainColor: "#ff3c2a", // 主题色
|
||||
lightColor: "#ff6b35", // 高亮主题色
|
||||
aiderLightColor: "#ff9f28", // 辅助高亮颜色
|
||||
defaultUserPhoto: "/static/missing-face.png", // 默认用户头像
|
||||
enableFetchMobileLogin: false // 是否启用获取手机号登录 如果微信小程序提示封禁手机号获取权限 可将此选项设置成false作为备用登录方案
|
||||
};
|
||||
|
||||
```
|
||||
#### 组件
|
||||
在根目录`components`下的`components`设置中配置了一些默认的组件,可以根据自己的需求进行修改或阅读组件详情代码以及README.md文档
|
||||
|
||||
#### 页面
|
||||
在`pages`文件夹写入或修改页面代码,在`pages.json`中去配置页面路由,具体配置可以参考`pages.json` 或参考uni-app的文档
|
||||
|
||||
在微信小程序中默认启用分包操作,如果需要在微信小程序中使用分包,需要在`pages.json`中配置分包路径,具体配置可以参考`pages.json` 或参考uni-app的文档
|
||||
|
||||
#### 主题
|
||||
1.现在`config`中设置主题色
|
||||
|
||||
2.在`uni.scss`中设置全局颜色
|
||||
|
||||
3.替换项目中一些icon以及图片的颜色
|
||||
|
||||
|
||||
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 微信小程序移动端启动报错
|
||||
|
||||
小程序直播插件报错,在`manifest`中删除代码。
|
||||

|
||||
|
||||
#### 微信小程序上传文件过大超出 2MB
|
||||
|
||||
1.关闭微信小程序,停止微信小程序运行
|
||||
|
||||
2.重新点击运行小程序模拟器,在最下方有一个 `运行时是否压缩代码` 勾选并重新运行
|
||||
|
||||
3.如果还是提示超出2MB,重新执行1步骤,然后点击`发行`->`小程序`即可
|
||||
|
||||
4.以上1-3步骤都不行,可以尝试删除一些自己开发的代码或本地的静态资源图片
|
||||
|
||||
#### 微信小程序在开发者工具中可以使用接口在测试版却不行
|
||||
1.首先保证是否填写`request`合法域名 在微信小程序后台配置
|
||||
|
||||
2.需保证接口为https请求
|
||||
|
||||
request合法域名基础配置如下
|
||||
```
|
||||
https://buyer-api.pickmall.cn
|
||||
https://common-api.pickmall.cn
|
||||
https://im-api.pickmall.cn
|
||||
https://restapi.amap.com
|
||||
```
|
||||
|
||||
socket合法域名
|
||||
```
|
||||
wss://im-api.pickmall.cn
|
||||
```
|
||||
uploadFile合法域名
|
||||
```
|
||||
https://common-api.pickmall.cn
|
||||
```
|
||||
downloadFile合法域名
|
||||
```
|
||||
https://buyer-api.pickmall.cn
|
||||
https://common-api.pickmall.cn
|
||||
https://im-api.pickmall.cn
|
||||
https://lilishop-oss.oss-cn-beijing.aliyuncs.com
|
||||
```
|
||||
|
||||
#### 如何配置API
|
||||
在根目录 `config` -> `api` 中配置开发环境以及生产环境 api 接口地址
|
||||
|
||||
#### 运行报错
|
||||
初次运行时候可能因为本地环境缺少编译插件,这个时候一定要注意看报错信息,根据报错信息安装相应的插件即可
|
||||
|
||||
#### 如何配置微信小程序appid
|
||||
在根目录 `manifest.json` 中配置微信小程序appid
|
||||
|
||||
|
||||
#### 微信登录报错
|
||||
需检查appId是否配置正确,在后台管理中信任登录的`appId`以及`appSecret`是否配置正确
|
||||
## 8. 升级计划
|
||||
|
||||
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
|
||||
|
||||
### ✅ 已完成特性
|
||||
| 功能模块 | 功能模块 |
|
||||
| :--- | :--- |
|
||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
||||
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | |
|
||||
|
||||
### 🔜 近期规划中
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
|
||||
- 🔜 OpenAPI 开放平台与开发者中心
|
||||
|
||||
@@ -37,7 +37,7 @@ export function getLogistics() {
|
||||
*/
|
||||
export function getAddressCode(cityCode, townName) {
|
||||
return http.request({
|
||||
url: api.common + "/common/region/region",
|
||||
url: `${api.buyer}/common/region/region`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params: { cityCode, townName },
|
||||
|
||||
43
api/appointment.js
Normal file
43
api/appointment.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 预约服务买家端 API
|
||||
*/
|
||||
import { http, Method } from '@/utils/request.js'
|
||||
|
||||
/** 查询可预约时段 */
|
||||
export function listSlots(skuId, slotDate) {
|
||||
return http.request({
|
||||
url: '/appointment/slots',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params: { skuId, slotDate },
|
||||
})
|
||||
}
|
||||
|
||||
/** 绑定预约结算上下文(addToCart 后、createTrade 前) */
|
||||
export function bindCheckout(data) {
|
||||
return http.request({
|
||||
url: '/appointment/checkout/bind',
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 预约商品扩展配置 */
|
||||
export function getGoodsExt(goodsId) {
|
||||
return http.request({
|
||||
url: `/appointment/goods/${goodsId}/ext`,
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
})
|
||||
}
|
||||
|
||||
/** 买家预约订单详情(时段、核销码) */
|
||||
export function getAppointmentOrderDetail(orderSn) {
|
||||
return http.request({
|
||||
url: `/appointment/order/${orderSn}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,5 +39,17 @@ export function getArticleDetail(type) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取文章列表(楼层装修用)
|
||||
* @param params { pageNumber, pageSize, categoryId? }
|
||||
*/
|
||||
export function getArticleList(params) {
|
||||
return http.request({
|
||||
url: `/other/article`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 公共API
|
||||
* 公共 API(由 buyer-api 提供)
|
||||
*/
|
||||
import { http, Method } from "@/utils/request.js";
|
||||
import api from "@/config/api.js";
|
||||
@@ -10,16 +10,16 @@ import api from "@/config/api.js";
|
||||
*/
|
||||
export function getRegionsById(id = 0) {
|
||||
return http.request({
|
||||
url: `${api.common}/common/region/item/${id}`,
|
||||
url: `${api.buyer}/common/region/item/${id}`,
|
||||
method: Method.GET,
|
||||
message: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM接口前缀
|
||||
// 获取 IM 接口前缀
|
||||
export function getIMDetail() {
|
||||
return http.request({
|
||||
url: `${api.common}/IM`,
|
||||
url: `${api.buyer}/common/IM`,
|
||||
method: Method.GET,
|
||||
message: false,
|
||||
});
|
||||
@@ -29,4 +29,26 @@ export function getIMDetail() {
|
||||
* 文件上传地址
|
||||
* @type {string}
|
||||
*/
|
||||
export const upload = api.common + "/common/upload/file";
|
||||
export const upload = `${api.buyer}/common/upload/file`;
|
||||
|
||||
/**
|
||||
* 获取主题色配置
|
||||
*/
|
||||
export function getThemeSetting() {
|
||||
return http.request({
|
||||
url: `${api.buyer}/common/site/theme`,
|
||||
method: Method.GET,
|
||||
message: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类页配置
|
||||
*/
|
||||
export function getCategoryPageSetting() {
|
||||
return http.request({
|
||||
url: `${api.buyer}/common/site/category-page`,
|
||||
method: Method.GET,
|
||||
message: false,
|
||||
});
|
||||
}
|
||||
|
||||
233
api/distribution.js
Normal file
233
api/distribution.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 分销员相关 API
|
||||
*/
|
||||
|
||||
import { http, Method } from '@/utils/request.js'
|
||||
|
||||
/**
|
||||
* 绑定分销员
|
||||
* @param distributionId 分销员ID
|
||||
*/
|
||||
export function getGoodsDistribution(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/bindingDistribution/${distributionId}`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会员的分销员信息(含待提现、冻结金额等)
|
||||
*/
|
||||
export function distribution() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广海报上下文(邀请好友锁客) */
|
||||
export function getPromotionPoster() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/promotion-poster`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 邀请卡上下文(招募下级分销员) */
|
||||
export function getInviteCard() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/invite-card`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 我邀请的下级分销员列表 */
|
||||
export function getDistributionInvitees(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/invite/invitees`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员业绩统计 */
|
||||
export function getDistributionPerformance(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/performance`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 我的客户列表 */
|
||||
export function getDistributionCustomers(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 客户详情 */
|
||||
export function getDistributionCustomerDetail(id) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers/${id}`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置/取消客户星标 */
|
||||
export function updateDistributionCustomerStarred(id, starred) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/customers/${id}/starred`,
|
||||
method: Method.PUT,
|
||||
params: { starred },
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销等级列表(包装 VO:upgradeConditionVisible + grades) */
|
||||
export function getDistributionGrades() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/grades`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员提现 */
|
||||
export function cash(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分销员提现历史 */
|
||||
export function cashLog(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广/邀请订单分页列表 */
|
||||
export function getDistributionOrders(params) {
|
||||
return http.request({
|
||||
url: `/distribution/order/orders`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 推广/邀请订单详情 */
|
||||
export function getDistributionOrderDetail(params) {
|
||||
return http.request({
|
||||
url: `/distribution/order/orders/detail`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品设置 */
|
||||
export function getDistributionGoodsSetting() {
|
||||
return http.request({
|
||||
url: `/distribution/goods/setting`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销员商品列表 */
|
||||
export function distributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销员中心推荐商品 */
|
||||
export function getDistributionFeaturedGoods() {
|
||||
return http.request({
|
||||
url: `/distribution/goods/featured`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品分享上下文(推广图文) */
|
||||
export function getDistributionGoodsShareContext(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/share-context`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取分销商品海报上下文(生成葵花码,服务端记录分享) */
|
||||
export function getDistributionGoodsPosterContext(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/poster-context`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 分享商品访问(记录访问并绑定客户关系) */
|
||||
export function recordDistributionGoodsVisit(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/share-visit`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/** 招募页信息 */
|
||||
export function getRecruitPage() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/page`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 招募页邀请人信息(无需登录) */
|
||||
export function getRecruitInviter(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/inviter`,
|
||||
method: Method.GET,
|
||||
params: { distributionId },
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交招募申请 */
|
||||
export function submitRecruitApplication(data) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/apply`,
|
||||
method: Method.POST,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/** 招募条件进度 */
|
||||
export function getRecruitProgress() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/progress`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
|
||||
/** 上报招募邀请 token(登录后供自动申请使用) */
|
||||
export function savePendingRecruitToken(recruitToken) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/recruit/pending-token`,
|
||||
method: Method.PUT,
|
||||
params: { recruitToken },
|
||||
})
|
||||
}
|
||||
|
||||
/** 上级分销员信息 */
|
||||
export function getDistributionParent() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/parent`,
|
||||
method: Method.GET,
|
||||
})
|
||||
}
|
||||
97
api/goods.js
97
api/goods.js
@@ -15,11 +15,11 @@ import { http, Method } from "@/utils/request.js";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ES中获取相关商品品牌名称,分类名称及属性
|
||||
* 获取商品搜索筛选条件
|
||||
*/
|
||||
export function getGoodsRelated(params) {
|
||||
return http.request({
|
||||
url: `/goods/goods/es/related`,
|
||||
url: `/goods/goods/search/related`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
@@ -37,18 +37,6 @@ export function getGoodsRelated(params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品分销
|
||||
* @param distributionId 商品分销ID
|
||||
*/
|
||||
export function getGoodsDistribution(distributionId) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/bindingDistribution/${distributionId}`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param params
|
||||
@@ -56,7 +44,7 @@ export function getGoodsRelated(params) {
|
||||
*/
|
||||
export function getGoodsList(params) {
|
||||
return http.request({
|
||||
url: "/goods/goods/es",
|
||||
url: "/goods/goods/search",
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
@@ -118,85 +106,6 @@ export function getCategoryList(id) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前会员的分销商信息 可根据分销商信息查询待提现金额以及冻结金额等信息
|
||||
*/
|
||||
export function distribution() {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请分销商
|
||||
*/
|
||||
export function applyDistribution(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现
|
||||
*/
|
||||
export function cash(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.POST,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现历史
|
||||
*/
|
||||
export function cashLog(params) {
|
||||
return http.request({
|
||||
url: `/distribution/cash`,
|
||||
method: Method.GET,
|
||||
params
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商分页订单列表
|
||||
*/
|
||||
export function distributionOrderList(params) {
|
||||
return http.request({
|
||||
url: `/distribution/distribution/distributionOrder`,
|
||||
method: Method.GET,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商商品列表
|
||||
*/
|
||||
export function distributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 选择分销商品 分销商品id
|
||||
*/
|
||||
export function checkedDistributionGoods(params) {
|
||||
return http.request({
|
||||
url: `/distribution/goods/checked/${params.id}`,
|
||||
method: Method.GET,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 小程序码
|
||||
*/
|
||||
|
||||
65
api/live.js
Normal file
65
api/live.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 直播流相关 API
|
||||
*/
|
||||
import { http, Method } from "@/utils/request.js";
|
||||
|
||||
/**
|
||||
* 通过 id 获取直播间详情
|
||||
* @param {string} id 直播流 ID
|
||||
*/
|
||||
export function getLiveRoomById(id) {
|
||||
return http.request({
|
||||
url: `live/room/${id}`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 观看的同时注册直播用户
|
||||
* @param {string} liveRoomId 直播流 ID
|
||||
*/
|
||||
export function registerLiveViewUser(liveRoomId) {
|
||||
return http.request({
|
||||
url: `live/user/view`,
|
||||
method: Method.POST,
|
||||
params: { liveRoomId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送直播消息
|
||||
* @param {object} data LiveMessage
|
||||
*/
|
||||
export function sendLiveMessage(data) {
|
||||
return http.request({
|
||||
url: `live/message`,
|
||||
method: Method.POST,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直播轮询聚合数据
|
||||
* @param {string} id 直播流 ID
|
||||
*/
|
||||
export function getLivePollingData(id) {
|
||||
return http.request({
|
||||
url: `live/room/polling/${id}`,
|
||||
method: Method.GET,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取直播间优惠券
|
||||
* @param {string} liveRoomId 直播流 ID
|
||||
* @param {string} couponId 优惠券 ID
|
||||
*/
|
||||
export function receiveLiveCoupon(liveRoomId, couponId) {
|
||||
return http.request({
|
||||
url: `promotion/coupon/receive/live/${liveRoomId}/${couponId}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
});
|
||||
}
|
||||
|
||||
17
api/login.js
17
api/login.js
@@ -59,7 +59,7 @@ export function getAutoCoup(){
|
||||
*/
|
||||
export function sendMobile(mobile,type='LOGIN') {
|
||||
return http.request({
|
||||
url: `${api.common}/common/sms/${type}/${mobile}`,
|
||||
url: `${api.buyer}/common/sms/${type}/${mobile}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
@@ -95,6 +95,19 @@ export function modifyPass(params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param newPassword
|
||||
* @param password
|
||||
*/
|
||||
export function resetPassword(params) {
|
||||
return http.request({
|
||||
url: `/passport/member/resetPassword`,
|
||||
method: "POST",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
@@ -138,4 +151,4 @@ export function logoffConfirm() {
|
||||
method: "PUT",
|
||||
needToken: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,23 @@ export function withdrawalApply(params) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getWithdrawApplyPage(params) {
|
||||
return http.request({
|
||||
url: "/member/withdrawApply",
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function getWithdrawApplyWechatTransferInfo(id) {
|
||||
return http.request({
|
||||
url: `/member/withdrawApply/wechat/transfer/${id}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果查询
|
||||
* @param orderType 交易类型,可用值:TRADE,ORDER,RECHARGE
|
||||
|
||||
@@ -110,3 +110,15 @@ export function getLogisticsMessages(params) {
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取微信公众号通知配置及绑定状态
|
||||
*/
|
||||
export function getWechatOfficialAccountSetting() {
|
||||
return http.request({
|
||||
url: '/message/wechatOfficialAccount',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,23 @@ export function getLiveList(params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取直播间详情
|
||||
* @deprecated 请使用 api/live.js 中的 getLiveRoomById
|
||||
* @param {number} roomId 直播间房间号
|
||||
*/
|
||||
export function getLiveDetail(roomId) {
|
||||
return http.request({
|
||||
url: `broadcast/studio`,
|
||||
method: Method.GET,
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
roomId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前拼团活动的未成团的会员
|
||||
*/
|
||||
@@ -185,4 +202,40 @@ export function getMineBargainLog(params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前客户礼品卡
|
||||
*/
|
||||
export function getGiftCardCashMemberCardPage(params) {
|
||||
return http.request({
|
||||
url: '/promotion/giftCardCash/memberCard',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定礼品卡(卡号 + 兑换码/卡密)
|
||||
*/
|
||||
export function bindGiftCardCashMemberCard(data) {
|
||||
return http.request({
|
||||
url: '/promotion/giftCardCash/memberCard/bind',
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
header: { 'content-type': 'application/json' },
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活礼品卡
|
||||
*/
|
||||
export function activateGiftCardCashMemberCard(memberCardId) {
|
||||
return http.request({
|
||||
url: `/promotion/giftCardCash/memberCard/${memberCardId}/activate`,
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
14
api/trade.js
14
api/trade.js
@@ -169,6 +169,20 @@ export function setAddressId(addressId,way) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 选择礼品卡
|
||||
* @param way 购物车购买:CART/立即购买:BUY_NOW/拼团:PINTUAN / 积分:POINTS / 砍价:KANJIA
|
||||
* @param credentialId 礼品卡凭证 id;used=false 且不传则清空全部
|
||||
*/
|
||||
export function selectGiftCard(params) {
|
||||
return http.request({
|
||||
url: `/trade/carts/select/giftCard`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建交易
|
||||
*/
|
||||
|
||||
133
components/category/category-scroller.vue
Normal file
133
components/category/category-scroller.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<scroll-view scroll-x scroll-with-animation class="cate-scroller" :scroll-into-view="intoView">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:id="'cs-' + item.id"
|
||||
class="cs-item"
|
||||
:class="{ active: item.id === currentId, 'cs-item--text': mode === 'text' }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
<template v-if="mode === 'icon'">
|
||||
<view class="cs-icon-box">
|
||||
<view class="cs-icon">
|
||||
<u-image width="80rpx" height="80rpx" shape="circle" :src="item.image" mode="aspectFill">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
</view>
|
||||
<text class="cs-name">{{ item.name }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="cs-text-name">{{ item.name }}</text>
|
||||
<view class="cs-text-line" v-if="item.id === currentId"></view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
mode?: 'icon' | 'text'
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null,
|
||||
mode: 'icon'
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
|
||||
const intoView = computed(() =>
|
||||
props.currentId != null ? 'cs-' + props.currentId : ''
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cate-scroller {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cs-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 130rpx;
|
||||
padding: 16rpx 6rpx 14rpx;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.cs-icon-box {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3rpx solid transparent;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.cs-item.active .cs-icon-box {
|
||||
border-color: $light-color;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 102, 0, 0.15);
|
||||
}
|
||||
.cs-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #f6f7f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cs-name {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
max-width: 120rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.cs-item.active .cs-name {
|
||||
color: $light-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cs-item--text {
|
||||
width: auto;
|
||||
padding: 18rpx 28rpx;
|
||||
position: relative;
|
||||
}
|
||||
.cs-text-name {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.cs-item--text.active .cs-text-name {
|
||||
color: $light-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cs-text-line {
|
||||
position: absolute;
|
||||
bottom: 6rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 32rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 2rpx;
|
||||
background: $light-color;
|
||||
}
|
||||
</style>
|
||||
75
components/category/category-side-list.vue
Normal file
75
components/category/category-side-list.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<scroll-view scroll-y scroll-with-animation class="side-list">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="side-item"
|
||||
:class="{ active: item.id === currentId }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
<view class="active-indicator" v-if="item.id === currentId"></view>
|
||||
<text class="side-item-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.side-list {
|
||||
width: 176rpx;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background-color: #f6f7f9;
|
||||
border-right: 1rpx solid #ebebeb;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.side-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 96rpx;
|
||||
padding: 16rpx 14rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
color: $light-color;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
.side-item-text {
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.active-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 0 4rpx 4rpx 0;
|
||||
background: $light-color;
|
||||
}
|
||||
</style>
|
||||
94
components/category/sort-bar.vue
Normal file
94
components/category/sort-bar.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<view class="sort-bar">
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'releaseTime' }"
|
||||
@click="onClick('releaseTime')"
|
||||
>
|
||||
<text class="sort-label">综合</text>
|
||||
</view>
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'buyCount' }"
|
||||
@click="onClick('buyCount')"
|
||||
>
|
||||
<text class="sort-label">销量</text>
|
||||
</view>
|
||||
<view
|
||||
class="sort-item"
|
||||
:class="{ active: sort === 'price' }"
|
||||
@click="onClick('price')"
|
||||
>
|
||||
<text class="sort-label">价格</text>
|
||||
<view class="arrows">
|
||||
<u-icon
|
||||
name="arrow-up-fill"
|
||||
size="7"
|
||||
:color="sort === 'price' && order === 'asc' ? '#ff6700' : '#bbbbbb'"
|
||||
></u-icon>
|
||||
<u-icon
|
||||
name="arrow-down-fill"
|
||||
size="7"
|
||||
:color="sort === 'price' && order === 'desc' ? '#ff6700' : '#bbbbbb'"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
sort?: string
|
||||
order?: string
|
||||
}>(), {
|
||||
sort: 'releaseTime',
|
||||
order: 'desc'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
function onClick(type: string) {
|
||||
let nextOrder = 'desc'
|
||||
if (type === 'price') {
|
||||
nextOrder = props.sort === 'price' && props.order === 'desc' ? 'asc' : 'desc'
|
||||
}
|
||||
emit('change', { sort: type, order: nextOrder })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sort-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 68rpx;
|
||||
background: #ffffff;
|
||||
padding: 0 20rpx;
|
||||
border-bottom: 1rpx solid #f6f6f6;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sort-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 44rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: $light-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.sort-label {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.arrows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8rpx;
|
||||
gap: 2rpx;
|
||||
}
|
||||
</style>
|
||||
61
components/category/sub-tag-bar.vue
Normal file
61
components/category/sub-tag-bar.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<scroll-view scroll-x scroll-with-animation class="tag-bar" :scroll-into-view="intoView">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:id="'tb-' + item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: item.id === currentId }"
|
||||
@click="$emit('change', item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[]
|
||||
currentId?: string | number | null
|
||||
}>(), {
|
||||
list: () => [],
|
||||
currentId: null
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
|
||||
const intoView = computed(() =>
|
||||
props.currentId != null ? 'tb-' + props.currentId : ''
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-bar {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
padding: 12rpx 16rpx 10rpx;
|
||||
border-bottom: 1rpx solid #f6f6f6;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tag-item {
|
||||
display: inline-block;
|
||||
padding: 6rpx 22rpx;
|
||||
margin-right: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
background: #f5f6f8;
|
||||
border-radius: 24rpx;
|
||||
line-height: 1.35;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: #ffffff;
|
||||
background: $light-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
297
components/category/tpl-category-goods.vue
Normal file
297
components/category/tpl-category-goods.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<view class="tpl-goods">
|
||||
<!-- 模板2/3:顶部一级分类图标横滑 -->
|
||||
<category-scroller
|
||||
v-if="template !== 4"
|
||||
:list="categoryList"
|
||||
:current-id="currentL1 && currentL1.id"
|
||||
mode="icon"
|
||||
@change="onL1Change"
|
||||
/>
|
||||
<view class="tpl-main">
|
||||
<!-- 左侧:模板2/3 为二级分类,模板4 为一级分类 -->
|
||||
<category-side-list
|
||||
:list="sideList"
|
||||
:current-id="sideCurrentId"
|
||||
@change="onSideChange"
|
||||
/>
|
||||
<view class="tpl-right">
|
||||
<!-- 模板2:三级分类标签横滑 (置于右栏顶部) -->
|
||||
<sub-tag-bar
|
||||
v-if="template === 2 && l3List.length"
|
||||
:list="l3List"
|
||||
:current-id="currentL3 && currentL3.id"
|
||||
@change="onL3Change"
|
||||
/>
|
||||
<!-- 模板4:右上二级分类文字横滑(起点为当前一级名称,作为"全部"入口) -->
|
||||
<category-scroller
|
||||
v-if="template === 4"
|
||||
:list="l2Items"
|
||||
:current-id="topCurrentId"
|
||||
mode="text"
|
||||
@change="onL2Change"
|
||||
/>
|
||||
<sort-bar :sort="sort" :order="order" @change="onSortChange" />
|
||||
<scroll-view scroll-y class="goods-scroll" @scrolltolower="loadMore">
|
||||
<m-goods-list
|
||||
v-if="goodsList.length"
|
||||
:res="goodsList"
|
||||
type="oneColumns"
|
||||
:show-fields="setting.showFields"
|
||||
:tab-bar-gap="false"
|
||||
:store-name="false"
|
||||
@add-cart="onAddCart"
|
||||
/>
|
||||
<view v-else-if="!loading" class="empty-state">
|
||||
<u-empty
|
||||
:text="failed ? '加载失败,请重试' : '暂无相关商品'"
|
||||
mode="data"
|
||||
icon="http://cdn.uviewui.com/uview/empty/data.png"
|
||||
margin-top="100"
|
||||
>
|
||||
<view v-if="failed" class="retry" @click="loadGoods(true)">重试</view>
|
||||
</u-empty>
|
||||
</view>
|
||||
<uni-load-more
|
||||
v-if="showLoadMore"
|
||||
:status="loading ? 'loading' : hasMore ? 'more' : 'noMore'"
|
||||
:color="'#bfbfbf'"
|
||||
:content-text="loadMoreText"
|
||||
></uni-load-more>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getGoodsList } from '@/api/goods.js'
|
||||
import CategoryScroller from './category-scroller.vue'
|
||||
import CategorySideList from './category-side-list.vue'
|
||||
import SubTagBar from './sub-tag-bar.vue'
|
||||
import SortBar from './sort-bar.vue'
|
||||
import MGoodsList from '@/components/m-goods-list/list.vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
categoryList?: any[]
|
||||
setting?: any
|
||||
template?: number
|
||||
}>(), {
|
||||
categoryList: () => [],
|
||||
setting: () => ({ template: 1, showFields: {} }),
|
||||
template: 2
|
||||
})
|
||||
|
||||
const currentL1 = ref<any>(null)
|
||||
const currentL2 = ref<any>(null)
|
||||
const currentL3 = ref<any>(null)
|
||||
const sort = ref('releaseTime')
|
||||
const order = ref('desc')
|
||||
|
||||
const goodsList = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const hasMore = ref(true)
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
|
||||
const loadMoreText = {
|
||||
contentdown: '上拉加载更多',
|
||||
contentrefresh: '正在加载...',
|
||||
contentnomore: '—— 到底啦 ——',
|
||||
}
|
||||
|
||||
// 仅在正在加载、或者有下一页、或者商品总数达到 6 件以上触底时才展示底线,避免少量商品(如1~2件)时在空白区域突兀展示
|
||||
const showLoadMore = computed(() => {
|
||||
if (!goodsList.value.length) return false
|
||||
if (loading.value) return true
|
||||
if (hasMore.value) return true
|
||||
return goodsList.value.length >= 6
|
||||
})
|
||||
|
||||
// 模板2/3:左侧为当前一级的二级分类;模板4:左侧为一级分类
|
||||
const sideList = computed(() =>
|
||||
props.template === 4
|
||||
? props.categoryList
|
||||
: (currentL1.value && currentL1.value.children) || []
|
||||
)
|
||||
const sideCurrentId = computed(() =>
|
||||
props.template === 4
|
||||
? currentL1.value && currentL1.value.id
|
||||
: currentL2.value && currentL2.value.id
|
||||
)
|
||||
// 模板2 的三级标签
|
||||
const l3List = computed(() =>
|
||||
(currentL2.value && currentL2.value.children) || []
|
||||
)
|
||||
// 模板4 顶部:当前一级名称(作为"全部") + 其二级分类
|
||||
const l2Items = computed(() => {
|
||||
if (!currentL1.value) return []
|
||||
return [currentL1.value, ...((currentL1.value.children) || [])]
|
||||
})
|
||||
const topCurrentId = computed(() =>
|
||||
(currentL2.value && currentL2.value.id) || (currentL1.value && currentL1.value.id)
|
||||
)
|
||||
|
||||
const queryCategoryId = computed(() =>
|
||||
(currentL3.value && currentL3.value.id) ||
|
||||
(currentL2.value && currentL2.value.id) ||
|
||||
(currentL1.value && currentL1.value.id)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.categoryList,
|
||||
(list) => {
|
||||
if (list && list.length && !currentL1.value) {
|
||||
currentL1.value = list[0]
|
||||
currentL2.value = props.template === 4 ? null : (list[0].children && list[0].children[0]) || null
|
||||
currentL3.value = (currentL2.value && currentL2.value.children && currentL2.value.children[0]) || null
|
||||
loadGoods(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function onL1Change(item: any) {
|
||||
if (currentL1.value && item.id === currentL1.value.id) return
|
||||
currentL1.value = item
|
||||
currentL2.value = props.template === 4 ? null : (item.children && item.children[0]) || null
|
||||
currentL3.value = (currentL2.value && currentL2.value.children && currentL2.value.children[0]) || null
|
||||
resetSort()
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onSideChange(item: any) {
|
||||
if (props.template === 4) {
|
||||
onL1Change(item)
|
||||
return
|
||||
}
|
||||
if (currentL2.value && item.id === currentL2.value.id) return
|
||||
currentL2.value = item
|
||||
currentL3.value = (item.children && item.children[0]) || null
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onL3Change(item: any) {
|
||||
if (currentL3.value && item.id === currentL3.value.id) return
|
||||
currentL3.value = item
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onL2Change(item: any) {
|
||||
// 选中一级名称项 = 全部(L2 置空)
|
||||
if (currentL1.value && item.id === currentL1.value.id) {
|
||||
currentL2.value = null
|
||||
} else {
|
||||
currentL2.value = item
|
||||
}
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function onSortChange(payload: { sort: string; order: string }) {
|
||||
sort.value = payload.sort
|
||||
order.value = payload.order
|
||||
loadGoods(true)
|
||||
}
|
||||
|
||||
function resetSort() {
|
||||
sort.value = 'releaseTime'
|
||||
order.value = 'desc'
|
||||
}
|
||||
|
||||
async function loadGoods(reset = false) {
|
||||
if (loading.value || !queryCategoryId.value) return
|
||||
if (reset) {
|
||||
pageNumber.value = 1
|
||||
goodsList.value = []
|
||||
hasMore.value = true
|
||||
}
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const res = await getGoodsList({
|
||||
categoryId: queryCategoryId.value,
|
||||
sort: sort.value,
|
||||
order: order.value,
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: 10,
|
||||
})
|
||||
const records = (res.data && res.data.result && res.data.result.records) || []
|
||||
goodsList.value.push(...records)
|
||||
hasMore.value = records.length >= 10
|
||||
} catch (e) {
|
||||
failed.value = true
|
||||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (!hasMore.value || loading.value) return
|
||||
pageNumber.value += 1
|
||||
loadGoods(false)
|
||||
}
|
||||
|
||||
function onAddCart(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/product/goods?id=' + item.id + '&goodsId=' + item.goodsId,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-goods {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tpl-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tpl-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
.goods-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0 100rpx;
|
||||
}
|
||||
.retry {
|
||||
margin-top: 24rpx;
|
||||
padding: 12rpx 50rpx;
|
||||
border: 1rpx solid $light-color;
|
||||
color: $light-color;
|
||||
border-radius: 36rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -10,40 +10,30 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// 购物车 cart 消息 msg 订单 order 查询 search
|
||||
type: {
|
||||
type: String,
|
||||
default: 'search'
|
||||
},
|
||||
isBtn:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title:{
|
||||
type: String,
|
||||
default: '没有相关内容'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
src:''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.src ='/static/default/default_'+ this.type + '.png';
|
||||
},
|
||||
methods: {
|
||||
toHome() {
|
||||
uni.switchTab({
|
||||
url: '/pages/home/home'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type?: string
|
||||
isBtn?: boolean
|
||||
title?: string
|
||||
}>(), {
|
||||
type: 'search',
|
||||
isBtn: false,
|
||||
title: '没有相关内容'
|
||||
})
|
||||
|
||||
const src = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
src.value = `/static/default/default_${props.type}.png`
|
||||
})
|
||||
|
||||
const toHome = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
459
components/distribution-goods-share/index.vue
Normal file
459
components/distribution-goods-share/index.vue
Normal file
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<view class="share-panel" :style="themeStyle">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'text' }"
|
||||
@click="activeTab = 'text'"
|
||||
>
|
||||
<text>推广图文</text>
|
||||
<view v-if="activeTab === 'text'" class="tab-line"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'poster' }"
|
||||
@click="activeTab = 'poster'"
|
||||
>
|
||||
<text>生成海报</text>
|
||||
<view v-if="activeTab === 'poster'" class="tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="activeTab === 'text'" class="content">
|
||||
<view class="text-card">
|
||||
<text v-if="shareContext.defaultShareText" class="share-text" :user-select="true">
|
||||
{{ shareContext.defaultShareText }}
|
||||
</text>
|
||||
<view class="share-divider"></view>
|
||||
<text class="share-text" :user-select="true">{{ shareBodyText }}</text>
|
||||
<view class="share-divider"></view>
|
||||
</view>
|
||||
|
||||
<view class="media-section">
|
||||
<scroll-view scroll-x class="image-scroll" v-if="images.length">
|
||||
<view class="image-list">
|
||||
<image
|
||||
v-for="(img, index) in images"
|
||||
:key="index"
|
||||
class="goods-image"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
@click="previewImage(index)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="save-btn-wrap">
|
||||
<view class="save-btn" @click="saveShareContent">保存图文</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="content">
|
||||
<poster-panel
|
||||
:images="images"
|
||||
:goods-name="shareContext.goodsName"
|
||||
:goods-desc="posterGoodsDesc"
|
||||
:price="shareContext.price"
|
||||
:sku-id="skuId"
|
||||
:goods-id="goodsId"
|
||||
:distribution-id="distributionId"
|
||||
:active="activeTab === 'poster'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import config from '@/config/config'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionGoodsShareContext } from '@/api/distribution'
|
||||
import { unitPrice, parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
import PosterPanel from './poster-panel.vue'
|
||||
// #ifdef H5
|
||||
import { h5Copy } from '@/js_sdk/h5-copy/h5-copy.js'
|
||||
// #endif
|
||||
|
||||
const props = defineProps<{
|
||||
skuId?: string
|
||||
goodsId?: string
|
||||
distributionId?: string
|
||||
}>()
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const activeTab = ref<'text' | 'poster'>('text')
|
||||
const loading = ref(true)
|
||||
const shareContext = ref<Record<string, any>>({})
|
||||
|
||||
const posterGoodsDesc = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
return ctx.goodsSellingPoint || ctx.defaultShareText || ''
|
||||
})
|
||||
|
||||
const images = computed(() => {
|
||||
const raw: string[] = []
|
||||
const list = shareContext.value.images
|
||||
if (Array.isArray(list) && list.length) {
|
||||
raw.push(...list)
|
||||
} else if (shareContext.value.thumbnail) {
|
||||
raw.push(shareContext.value.thumbnail)
|
||||
}
|
||||
const parsed = raw
|
||||
.map((item) => parseGoodsImageUrl(item))
|
||||
.filter((url) => !!url)
|
||||
return [...new Set(parsed)]
|
||||
})
|
||||
|
||||
const shareLink = computed(() => {
|
||||
const link = shareContext.value.miniProgramLink || shareContext.value.sharePath || ''
|
||||
if (!link) return ''
|
||||
if (link.startsWith('http')) return link
|
||||
const base = (config.shareLink || '').replace(/\/$/, '')
|
||||
return base + link
|
||||
})
|
||||
|
||||
const SHARE_DIVIDER = '- '.repeat(36).trim()
|
||||
|
||||
const shareBodyText = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
const lines: string[] = []
|
||||
if (ctx.goodsName) {
|
||||
lines.push(`🔴 ${ctx.goodsName}`)
|
||||
}
|
||||
if (ctx.price !== null && ctx.price !== undefined) {
|
||||
lines.push(`【到手价】💰 ${unitPrice(ctx.price, '¥')}元`)
|
||||
}
|
||||
if (shareLink.value) {
|
||||
lines.push(`点击立即参与:${shareLink.value}`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
const shareText = computed(() => {
|
||||
const ctx = shareContext.value
|
||||
const lines: string[] = []
|
||||
if (ctx.defaultShareText) {
|
||||
lines.push(ctx.defaultShareText)
|
||||
lines.push('')
|
||||
}
|
||||
lines.push(SHARE_DIVIDER)
|
||||
if (shareBodyText.value) {
|
||||
lines.push(shareBodyText.value)
|
||||
}
|
||||
lines.push(SHARE_DIVIDER)
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.skuId, props.goodsId, props.distributionId],
|
||||
() => {
|
||||
loadShareContext()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function loadShareContext() {
|
||||
const { skuId, goodsId, distributionId } = props
|
||||
if (!skuId || !goodsId || !distributionId) {
|
||||
loading.value = false
|
||||
shareContext.value = {}
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
getDistributionGoodsShareContext({ skuId, goodsId, distributionId })
|
||||
.then((res) => {
|
||||
if (res.data?.success) {
|
||||
shareContext.value = res.data.result || {}
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载分享内容失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function previewImage(index: number) {
|
||||
if (!images.value.length) return
|
||||
uni.previewImage({
|
||||
current: images.value[index],
|
||||
urls: images.value,
|
||||
})
|
||||
}
|
||||
|
||||
function isClipboardBlocked(errMsg = '') {
|
||||
return (
|
||||
errMsg.includes('privacy agreement') ||
|
||||
errMsg.includes('no permission') ||
|
||||
errMsg.includes('auth deny')
|
||||
)
|
||||
}
|
||||
|
||||
function saveShareContent() {
|
||||
const text = shareText.value
|
||||
if (!text.trim()) {
|
||||
uni.showToast({ title: '暂无可保存内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
if (h5Copy(text) === false) {
|
||||
uni.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
continueAfterCopy()
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
const runCopy = () => {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
showToast: false,
|
||||
success: () => continueAfterCopy(),
|
||||
fail: (err: any) => {
|
||||
console.error('setClipboardData fail', err)
|
||||
const errMsg = err?.errMsg || ''
|
||||
if (isClipboardBlocked(errMsg)) {
|
||||
continueAfterCopy()
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const wxApi = (globalThis as any).wx
|
||||
if (wxApi?.requirePrivacyAuthorize) {
|
||||
wxApi.requirePrivacyAuthorize({
|
||||
success: () => runCopy(),
|
||||
fail: () => continueAfterCopy(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// #endif
|
||||
runCopy()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function continueAfterCopy() {
|
||||
const imageUrls = images.value
|
||||
if (!imageUrls.length) {
|
||||
uni.showToast({ title: '内容已复制', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '保存中', mask: true })
|
||||
saveImages(imageUrls)
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '内容已复制', icon: 'none' })
|
||||
})
|
||||
.catch((error) => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '图片保存失败', icon: 'none' })
|
||||
console.error('save images failed', error)
|
||||
})
|
||||
}
|
||||
|
||||
function saveToAlbum(filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => resolve(),
|
||||
fail: (err: any) => {
|
||||
const errMsg = err?.errMsg || ''
|
||||
const needAuth =
|
||||
errMsg.includes('auth deny') ||
|
||||
errMsg.includes('authorize') ||
|
||||
errMsg.includes('permission')
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!needAuth) {
|
||||
reject(new Error('save failed'))
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) {
|
||||
reject(new Error('auth denied'))
|
||||
return
|
||||
}
|
||||
uni.openSetting({
|
||||
success: (settingRes) => {
|
||||
if (settingRes.authSetting['scope.writePhotosAlbum']) {
|
||||
saveToAlbum(filePath).then(resolve).catch(reject)
|
||||
} else {
|
||||
reject(new Error('auth denied'))
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error('auth denied')),
|
||||
})
|
||||
},
|
||||
fail: () => reject(new Error('auth denied')),
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error('save failed'))
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveImages(urls: string[]) {
|
||||
return urls.reduce<Promise<void>>((chain, url) => {
|
||||
return chain.then(() => downloadAndSaveImage(url))
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
function downloadAndSaveImage(url: string) {
|
||||
if (!url || !/^https?:\/\//.test(url)) {
|
||||
return Promise.reject(new Error('invalid url'))
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||
reject(new Error('download failed'))
|
||||
return
|
||||
}
|
||||
saveToAlbum(res.tempFilePath).then(resolve).catch(reject)
|
||||
},
|
||||
fail: () => reject(new Error('download failed')),
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.share-panel {
|
||||
min-height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 32rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-bottom: 20rpx;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
|
||||
&.active {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 56rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.text-card {
|
||||
background: #f7f7f7;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.share-divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin: 20rpx 0;
|
||||
border-top: 2rpx dashed #c8c8c8;
|
||||
}
|
||||
|
||||
.share-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.media-section {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.image-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.save-btn-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
min-width: 140rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 6rpx 16rpx rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
</style>
|
||||
556
components/distribution-goods-share/poster-panel.vue
Normal file
556
components/distribution-goods-share/poster-panel.vue
Normal file
@@ -0,0 +1,556 @@
|
||||
<template>
|
||||
<view class="poster-panel">
|
||||
<view v-if="posterList.length" class="poster-hint">点击海报预览后可保存到相册</view>
|
||||
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">海报生成中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loadError" class="loading-wrap">
|
||||
<text class="loading-text">{{ loadError }}</text>
|
||||
<view class="retry-btn" @click="generatePosters">重新生成</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!images.length" class="loading-wrap">
|
||||
<text class="loading-text">暂无商品图片</text>
|
||||
</view>
|
||||
|
||||
<template v-else-if="posterList.length">
|
||||
<view class="poster-content">
|
||||
<swiper
|
||||
class="poster-swiper"
|
||||
:current="current"
|
||||
circular
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(item, index) in posterList" :key="index">
|
||||
<view class="poster-card-wrap" @click="previewPoster(index)">
|
||||
<image class="poster-image" :src="item" mode="widthFix" />
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-if="posterList.length > 1" class="poster-dots">
|
||||
<view
|
||||
v-for="(_, index) in posterList"
|
||||
:key="index"
|
||||
:class="['dot', current === index && 'active']"
|
||||
/>
|
||||
</view>
|
||||
<view class="save-btn-wrap">
|
||||
<view class="save-btn" @click="saveCurrentPoster">保存海报</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas id="distributionPosterCanvas" type="2d" style="width: 600px; height: 840px" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas
|
||||
canvas-id="distributionPosterCanvas"
|
||||
id="distributionPosterCanvas"
|
||||
style="width: 600px; height: 840px"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, getCurrentInstance, nextTick } from 'vue'
|
||||
import DrawPoster from '@/js_sdk/u-draw-poster'
|
||||
import { getDistributionGoodsPosterContext } from '@/api/distribution'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
|
||||
const CANVAS_SELECTOR = 'distributionPosterCanvas'
|
||||
const POSTER_WIDTH = 600
|
||||
const IMAGE_HEIGHT = POSTER_WIDTH
|
||||
const BOTTOM_HEIGHT = 240
|
||||
const POSTER_HEIGHT = IMAGE_HEIGHT + BOTTOM_HEIGHT
|
||||
const QR_SIZE = 120
|
||||
const QR_MARGIN_RIGHT = 28
|
||||
const QR_MARGIN_BOTTOM = 28
|
||||
const GENERATE_TIMEOUT = 45000
|
||||
|
||||
const props = defineProps<{
|
||||
images?: string[]
|
||||
goodsName?: string
|
||||
goodsDesc?: string
|
||||
price?: number | null
|
||||
skuId?: string
|
||||
goodsId?: string
|
||||
distributionId?: string
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const posterList = ref<string[]>([])
|
||||
const current = ref(0)
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const hasGenerated = ref(false)
|
||||
let dp: any = null
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const st2 = (size: number) => size * 2
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const st2 = (size: number) => size
|
||||
// #endif
|
||||
|
||||
watch(
|
||||
() => [props.images?.length, props.skuId, props.goodsId, props.distributionId],
|
||||
() => {
|
||||
resetPosters()
|
||||
if (props.active && props.images?.length) {
|
||||
generatePosters()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(active) => {
|
||||
if (active && !hasGenerated.value && props.images?.length && !loading.value) {
|
||||
generatePosters()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function resetPosters() {
|
||||
hasGenerated.value = false
|
||||
posterList.value = []
|
||||
loadError.value = ''
|
||||
clearDrawPosterCache()
|
||||
}
|
||||
|
||||
function clearDrawPosterCache() {
|
||||
const pages = getCurrentPages()
|
||||
const page = pages[pages.length - 1] as any
|
||||
if (!page) return
|
||||
delete page[`#${CANVAS_SELECTOR}__dp`]
|
||||
delete page[`${CANVAS_SELECTOR}__dp`]
|
||||
}
|
||||
|
||||
function onSwiperChange(e: any) {
|
||||
current.value = e.detail?.current ?? 0
|
||||
}
|
||||
|
||||
function previewPoster(index: number) {
|
||||
if (!posterList.value.length) return
|
||||
uni.previewImage({
|
||||
current: posterList.value[index],
|
||||
urls: posterList.value,
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number, message: string) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(message)), ms)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
function getComponentThis() {
|
||||
return instance?.proxy || instance
|
||||
}
|
||||
|
||||
async function waitCanvasReady() {
|
||||
await nextTick()
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
async function resolveImagePath(url: string) {
|
||||
if (!url) {
|
||||
throw new Error('商品图片地址无效')
|
||||
}
|
||||
if (
|
||||
url.startsWith('data:') ||
|
||||
url.startsWith('wxfile://') ||
|
||||
url.startsWith('http://tmp/') ||
|
||||
url.startsWith('https://tmp/')
|
||||
) {
|
||||
return url
|
||||
}
|
||||
const res = await uni.downloadFile({ url })
|
||||
if (res.statusCode === 200 && res.tempFilePath) {
|
||||
return res.tempFilePath
|
||||
}
|
||||
throw new Error('商品图片下载失败,请检查图片域名配置')
|
||||
}
|
||||
|
||||
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => resolve({ width: res.width, height: res.height }),
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function calcContainBox(
|
||||
imgW: number,
|
||||
imgH: number,
|
||||
boxX: number,
|
||||
boxY: number,
|
||||
boxW: number,
|
||||
boxH: number
|
||||
) {
|
||||
const scale = Math.min(boxW / imgW, boxH / imgH)
|
||||
const width = imgW * scale
|
||||
const height = imgH * scale
|
||||
return {
|
||||
x: boxX + (boxW - width) / 2,
|
||||
y: boxY + (boxH - height) / 2,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePosters() {
|
||||
const { skuId, goodsId, distributionId, images } = props
|
||||
if (!skuId || !goodsId || !distributionId || !images?.length) {
|
||||
return
|
||||
}
|
||||
if (loading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
{
|
||||
loadError.value = '请在微信小程序中生成海报'
|
||||
return
|
||||
}
|
||||
// #endif
|
||||
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
posterList.value = []
|
||||
clearDrawPosterCache()
|
||||
|
||||
try {
|
||||
await withTimeout(runGeneratePosters(images, skuId, goodsId, distributionId), GENERATE_TIMEOUT, '海报生成超时,请重试')
|
||||
hasGenerated.value = true
|
||||
} catch (error: any) {
|
||||
console.error('generate posters failed', error)
|
||||
loadError.value = error?.message || '海报生成失败,请稍后重试'
|
||||
clearDrawPosterCache()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runGeneratePosters(
|
||||
images: string[],
|
||||
skuId: string,
|
||||
goodsId: string,
|
||||
distributionId: string
|
||||
) {
|
||||
const posterRes = await getDistributionGoodsPosterContext({ skuId, goodsId, distributionId })
|
||||
if (!posterRes.data?.success || !posterRes.data?.result?.qrCode) {
|
||||
throw new Error(posterRes.data?.message || '葵花码生成失败')
|
||||
}
|
||||
const qrCode = `data:image/png;base64,${posterRes.data.result.qrCode}`
|
||||
|
||||
await waitCanvasReady()
|
||||
|
||||
dp = await DrawPoster.build({
|
||||
selector: CANVAS_SELECTOR,
|
||||
componentThis: getComponentThis(),
|
||||
loading: false,
|
||||
debugging: false,
|
||||
})
|
||||
|
||||
if (!dp?.canvas || !dp?.ctx) {
|
||||
throw new Error('画布初始化失败,请重试')
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
dp.canvas.width = st2(POSTER_WIDTH)
|
||||
dp.canvas.height = st2(POSTER_HEIGHT)
|
||||
// #endif
|
||||
|
||||
const results: string[] = []
|
||||
for (const image of images) {
|
||||
const localImage = await resolveImagePath(image)
|
||||
results.push(await drawPoster(dp, localImage, qrCode))
|
||||
}
|
||||
posterList.value = results
|
||||
}
|
||||
|
||||
async function drawPoster(dpInst: any, productImg: string, qrCode: string) {
|
||||
const goodsName = props.goodsName || '商品推荐'
|
||||
const goodsDesc = resolveGoodsDesc()
|
||||
const priceText =
|
||||
props.price !== null && props.price !== undefined ? unitPrice(props.price, '¥') : ''
|
||||
|
||||
await dpInst.draw((ctx: any) => {
|
||||
ctx.clearRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT))
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRoundRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(POSTER_HEIGHT), st2(20))
|
||||
ctx.clip()
|
||||
})
|
||||
|
||||
await dpInst.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = '#f7f7f7'
|
||||
ctx.fillRect(st2(0), st2(0), st2(POSTER_WIDTH), st2(IMAGE_HEIGHT))
|
||||
const { width: imgW, height: imgH } = await getImageInfo(productImg)
|
||||
const fit = calcContainBox(imgW, imgH, 0, 0, POSTER_WIDTH, IMAGE_HEIGHT)
|
||||
await ctx.drawImage(
|
||||
productImg,
|
||||
st2(fit.x),
|
||||
st2(fit.y),
|
||||
st2(fit.width),
|
||||
st2(fit.height)
|
||||
)
|
||||
})
|
||||
|
||||
await dpInst.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(st2(0), st2(IMAGE_HEIGHT), st2(POSTER_WIDTH), st2(POSTER_HEIGHT - IMAGE_HEIGHT))
|
||||
|
||||
const qrX = POSTER_WIDTH - QR_SIZE - QR_MARGIN_RIGHT
|
||||
const qrY = POSTER_HEIGHT - QR_SIZE - QR_MARGIN_BOTTOM
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(
|
||||
st2(qrX + QR_SIZE / 2),
|
||||
st2(qrY + QR_SIZE / 2),
|
||||
st2(QR_SIZE / 2),
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
ctx.clip()
|
||||
await ctx.drawImage(qrCode, st2(qrX), st2(qrY), st2(QR_SIZE), st2(QR_SIZE))
|
||||
ctx.restore()
|
||||
})
|
||||
|
||||
await dpInst.draw((ctx: any) => {
|
||||
const textLeft = 24
|
||||
const textMaxWidth = POSTER_WIDTH - QR_SIZE - QR_MARGIN_RIGHT - textLeft - 20
|
||||
const bottomTop = IMAGE_HEIGHT
|
||||
const bottomBottom = POSTER_HEIGHT - 16
|
||||
const qrY = POSTER_HEIGHT - QR_SIZE - QR_MARGIN_BOTTOM
|
||||
|
||||
const nameLineHeight = 36
|
||||
const descLineHeight = 28
|
||||
const priceLineHeight = 36
|
||||
const textGap = 16
|
||||
const nameLineCount = Math.min(2, Math.max(1, Math.ceil(goodsName.length / 16)))
|
||||
const descLineCount = goodsDesc ? Math.min(2, Math.max(1, Math.ceil(goodsDesc.length / 14))) : 0
|
||||
|
||||
let textBlockHeight = nameLineCount * nameLineHeight
|
||||
if (goodsDesc) {
|
||||
textBlockHeight += textGap + descLineCount * descLineHeight
|
||||
}
|
||||
if (priceText) {
|
||||
textBlockHeight += textGap + priceLineHeight
|
||||
}
|
||||
|
||||
const maxTextHeight = bottomBottom - bottomTop - 12
|
||||
if (textBlockHeight > maxTextHeight) {
|
||||
textBlockHeight = maxTextHeight
|
||||
}
|
||||
|
||||
let textY = qrY + (QR_SIZE - textBlockHeight) / 2
|
||||
textY = Math.max(bottomTop + 12, textY)
|
||||
if (textY + textBlockHeight > bottomBottom) {
|
||||
textY = bottomBottom - textBlockHeight
|
||||
}
|
||||
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillStyle = '#333333'
|
||||
ctx.font = `${st2(28)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: goodsName,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(textLeft),
|
||||
y: st2(textY),
|
||||
layer: 2,
|
||||
lineHeight: st2(nameLineHeight),
|
||||
})
|
||||
textY += nameLineCount * nameLineHeight
|
||||
if (goodsDesc) {
|
||||
textY += textGap
|
||||
ctx.fillStyle = '#999999'
|
||||
ctx.font = `${st2(22)}px PingFang SC`
|
||||
ctx.fillWarpText({
|
||||
text: goodsDesc,
|
||||
maxWidth: st2(textMaxWidth),
|
||||
x: st2(textLeft),
|
||||
y: st2(textY),
|
||||
layer: 2,
|
||||
lineHeight: st2(descLineHeight),
|
||||
})
|
||||
textY += descLineCount * descLineHeight
|
||||
}
|
||||
|
||||
if (priceText) {
|
||||
textY += textGap
|
||||
const priceY = Math.min(textY, bottomBottom - priceLineHeight)
|
||||
ctx.fillStyle = '#ff3c2a'
|
||||
ctx.font = `bold ${st2(36)}px PingFang SC`
|
||||
ctx.fillText(priceText, st2(textLeft), st2(priceY))
|
||||
}
|
||||
})
|
||||
|
||||
const path = await dpInst.createImagePath()
|
||||
if (!path || path === '---stop createImagePath---') {
|
||||
throw new Error('海报图片导出失败')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
function resolveGoodsDesc() {
|
||||
const desc = (props.goodsDesc || '').trim()
|
||||
const name = (props.goodsName || '').trim()
|
||||
if (!desc || desc === name) return ''
|
||||
return desc.length > 28 ? `${desc.slice(0, 28)}...` : desc
|
||||
}
|
||||
|
||||
function saveCurrentPoster() {
|
||||
const filePath = posterList.value[current.value]
|
||||
if (!filePath) return
|
||||
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已保存到相册', icon: 'none' })
|
||||
},
|
||||
fail: (err: any) => {
|
||||
const errMsg = err?.errMsg || ''
|
||||
if (errMsg.includes('auth deny') || errMsg.includes('authorize')) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
uni.openSetting({})
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.poster-panel {
|
||||
padding: 8rpx 0 48rpx;
|
||||
}
|
||||
|
||||
.poster-hint {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #b3b3b3;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.poster-content {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.retry-btn,
|
||||
.save-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: 32rpx;
|
||||
background: linear-gradient(90deg, #ff9f3f, #ff6b35);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.poster-swiper {
|
||||
height: 1000rpx;
|
||||
}
|
||||
|
||||
.poster-card-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0 48rpx;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.poster-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
|
||||
&.active {
|
||||
width: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #ff8f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.save-btn-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.canvas-hide {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
width: 600px;
|
||||
height: 840px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -13,71 +13,62 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from "@/config/config";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config, // 设置工具类
|
||||
weChat: false, // 是否微信浏览器,该项为true时不显示 当前整个页面
|
||||
logo: require("@/icon.png"), //显示的圆形logo
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef H5
|
||||
// 判断是否是微信浏览器
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var isWeixin = ua.indexOf("micromessenger") != -1;
|
||||
if (isWeixin) {
|
||||
this.weChat = true;
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import config from "@/config/config"
|
||||
import logoImg from "@/icon.png"
|
||||
|
||||
const weChat = ref(false)
|
||||
const logo = logoImg
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef H5
|
||||
// 判断是否是微信浏览器
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var isWeixin = ua.indexOf("micromessenger") != -1;
|
||||
weChat.value = isWeixin ? true : false
|
||||
// #endif
|
||||
})
|
||||
|
||||
/**
|
||||
* 跳转到下载app页面
|
||||
*/
|
||||
const downloadApp = () => {
|
||||
setTimeout(function () {
|
||||
window.location.href = config.downloadLink;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开app 仅在h5生效 使用ifream唤醒app
|
||||
*/
|
||||
const openApp = () => {
|
||||
let src: string = ''
|
||||
if (location.href) {
|
||||
src = location.href.split("/pages")[1];
|
||||
}
|
||||
let t = `${config.schemeLink}pages${src}`
|
||||
|
||||
try {
|
||||
var e = navigator.userAgent.toLowerCase(),
|
||||
n = e.match(/cpu iphone os (.*?) like mac os/);
|
||||
if (
|
||||
((n = null !== n ? n[1].replace(/_/g, ".") : 0), parseInt(n) >= 9)
|
||||
) {
|
||||
window.location.href = t
|
||||
|
||||
downloadApp()
|
||||
} else {
|
||||
this.weChat = false;
|
||||
var r = document.createElement("iframe");
|
||||
(r.src = t), (r.style.display = "none"), document.body.appendChild(r)
|
||||
|
||||
downloadApp()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 跳转到下载app页面
|
||||
*/
|
||||
downloadApp() {
|
||||
setTimeout(function () {
|
||||
window.location.href = config.downloadLink;
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开app 仅在h5生效 使用ifream唤醒app
|
||||
*/
|
||||
openApp() {
|
||||
let src;
|
||||
if (location.href) {
|
||||
src = location.href.split("/pages")[1];
|
||||
}
|
||||
let t = `${config.schemeLink}pages${src}`;
|
||||
|
||||
try {
|
||||
var e = navigator.userAgent.toLowerCase(),
|
||||
n = e.match(/cpu iphone os (.*?) like mac os/);
|
||||
if (
|
||||
((n = null !== n ? n[1].replace(/_/g, ".") : 0), parseInt(n) >= 9)
|
||||
) {
|
||||
window.location.href = t;
|
||||
|
||||
this.downloadApp();
|
||||
} else {
|
||||
var r = document.createElement("iframe");
|
||||
(r.src = t), (r.style.display = "none"), document.body.appendChild(r);
|
||||
|
||||
this.downloadApp();
|
||||
}
|
||||
} catch (e) {
|
||||
window.location.href = t;
|
||||
this.downloadApp();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
window.location.href = t
|
||||
downloadApp()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<u-popup class="popup" v-model="buyMask" :height="setup.height" closeable :mode="setup.mode" :border-radius="setup.radius" @close="closeMask()">
|
||||
<u-popup class="popup" :show="buyMask" :height="setup.height" closeable :mode="setup.mode" :border-radius="setup.radius" @close="closeMask()">
|
||||
<!-- 商品 -->
|
||||
<view class="goods-box bottom">
|
||||
<view class="goods-header">
|
||||
@@ -12,8 +12,8 @@
|
||||
<view class="goods-price" v-if="goodsDetail.promotionPrice && ((isGroup && buyType === 'PINTUAN') || !isGroup)">
|
||||
<span v-if="goodsDetail.promotionPrice && !pointDetail">
|
||||
¥
|
||||
<span class="goods-price-promotionShow goods-price-bigshow">{{ $options.filters.goodsFormatPrice(goodsDetail.promotionPrice)[0] }}</span>
|
||||
.{{ $options.filters.goodsFormatPrice(goodsDetail.promotionPrice)[1] }}
|
||||
<span class="goods-price-promotionShow goods-price-bigshow">{{ goodsFormatPrice(goodsDetail.promotionPrice)[0] }}</span>
|
||||
.{{ goodsFormatPrice(goodsDetail.promotionPrice)[1] }}
|
||||
</span>
|
||||
<span v-if="pointDetail.points">
|
||||
<span class="goods-price-promotionShow goods-price-bigshow">{{ pointDetail.points }}</span>
|
||||
@@ -21,28 +21,17 @@
|
||||
</span>
|
||||
<div class="promotion-box">
|
||||
¥
|
||||
<span class="goods-price-bigshow">{{ $options.filters.goodsFormatPrice(goodsDetail.price)[0] }}</span>
|
||||
.{{ $options.filters.goodsFormatPrice(goodsDetail.price)[1] }}
|
||||
<span class="goods-price-bigshow">{{ goodsFormatPrice(goodsDetail.price)[0] }}</span>
|
||||
.{{ goodsFormatPrice(goodsDetail.price)[1] }}
|
||||
</div>
|
||||
</view>
|
||||
<!-- 正常商品的价格 -->
|
||||
<view v-else>
|
||||
<!-- 批发价格 -->
|
||||
<div class="price-row flex" v-if="goodsDetail.salesModel === 'WHOLESALE'">
|
||||
<div class="goods-price" v-for="(item, index) in wholesaleList" :key="index">
|
||||
<span>
|
||||
¥
|
||||
<span class="goods-price-bigshow">{{ $options.filters.goodsFormatPrice(item.price)[0] }}</span>
|
||||
.{{ $options.filters.goodsFormatPrice(item.price)[1] }}
|
||||
</span>
|
||||
<span class="wholesale-item">{{ item.num }}{{ goodsDetail.goodsUnit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-price" v-else>
|
||||
<div class="goods-price">
|
||||
<span>
|
||||
¥
|
||||
<span class="goods-price-bigshow">{{ $options.filters.goodsFormatPrice(goodsDetail.price)[0] }}</span>
|
||||
.{{ $options.filters.goodsFormatPrice(goodsDetail.price)[1] }}
|
||||
<span class="goods-price-bigshow">{{ goodsFormatPrice(goodsDetail.price)[0] }}</span>
|
||||
.{{ goodsFormatPrice(goodsDetail.price)[1] }}
|
||||
</span>
|
||||
</div>
|
||||
</view>
|
||||
@@ -53,10 +42,14 @@
|
||||
<span>,{{ num }}个</span>
|
||||
</span>
|
||||
</view>
|
||||
<view class="goods-check-skus">
|
||||
<view class="goods-check-skus" v-if="isAppointmentGoods(goodsDetail?.goodsType)">
|
||||
可预约
|
||||
<span class="goods-check-skus-name">按时段选择</span>
|
||||
</view>
|
||||
<view class="goods-check-skus" v-else>
|
||||
库存
|
||||
<span class="goods-check-skus-name">
|
||||
<span>{{ goodsDetail.quantity }}</span>
|
||||
<span>{{ availableStock }}</span>
|
||||
</span>
|
||||
</view>
|
||||
</view>
|
||||
@@ -81,345 +74,365 @@
|
||||
</view>
|
||||
|
||||
<!-- 拼团购买,仅筛选出当前拼团类型商品 -->
|
||||
<view
|
||||
v-if="parentOrder && spec_val.skuId == goodsDetail.id"
|
||||
:class="{ active: spec_val.value == currentSelected[specIndex] }"
|
||||
class="skus-view-item"
|
||||
v-for="(spec_val, spec_index) in spec.values"
|
||||
:key="spec_index"
|
||||
@click="handleClickSpec(spec, specIndex, spec_val)"
|
||||
>
|
||||
{{ spec_val.value }}
|
||||
</view>
|
||||
<template v-for="(spec_val, spec_index) in spec.values" :key="spec_index">
|
||||
<view
|
||||
v-if="parentOrder && spec_val.skuId == goodsDetail.id"
|
||||
:class="{ active: spec_val.value == currentSelected[specIndex] }"
|
||||
class="skus-view-item"
|
||||
@click="handleClickSpec(spec, specIndex, spec_val)"
|
||||
>
|
||||
{{ spec_val.value }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<div class="soldout" v-if="goodsDetail.quantity === 0">
|
||||
<u-alert-tips type="warning" title="商品已售罄" description="当前商品库存为0"></u-alert-tips>
|
||||
<div class="soldout" v-if="availableStock === 0">
|
||||
<u-alert type="warning" title="商品已售罄" :description="'当前商品库存为0'"></u-alert>
|
||||
</div>
|
||||
<!-- 数量 -->
|
||||
<view v-if="goodsDetail.quantity !== 0" class="goods-skus-number flex flex-a-c flex-j-sb">
|
||||
<view v-if="availableStock !== 0" class="goods-skus-number flex flex-a-c flex-j-sb">
|
||||
<view class="view-class-title">数量</view>
|
||||
<uni-number-box class="uNumber" :min="1" :max="999" :disabled="goodsDetail.quantity === 0" v-model="num"></uni-number-box>
|
||||
<uni-number-box class="uNumber" :min="1" :max="maxBuyNum" :disabled="availableStock === 0" v-model="num"></uni-number-box>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 按钮 -->
|
||||
<view class="btns" v-if="goodsDetail.quantity !== 0">
|
||||
<view class="box-btn card" v-if="buyType != 'PINTUAN' && goodsDetail.goodsType != 'VIRTUAL_GOODS'" @click="addToCartOrBuy('cart')">加入购物车</view>
|
||||
<view class="box-btn buy" @click="addToCartOrBuy('buy')">立即购买</view>
|
||||
<view class="btns" v-if="availableStock !== 0">
|
||||
<view class="box-btn card" v-if="showAddToCart" @click="addToCartOrBuy('cart')">加入购物车</view>
|
||||
<view class="box-btn buy" @click="addToCartOrBuy('buy')">
|
||||
{{ isAppointmentGoods(goodsDetail?.goodsType) ? '立即预约' : '立即购买' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as API_trade from '@/api/trade.js';
|
||||
import setup from './popup';
|
||||
import uniNumberBox from '@/components/uni-number-box'
|
||||
export default {
|
||||
components: {
|
||||
uniNumberBox
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
setup,
|
||||
num: this.wholesaleList && this.wholesaleList.length > 0 ? this.wholesaleList[0].num : 1,
|
||||
|
||||
selectName: '', //选中商品的昵称
|
||||
selectSkuList: '', //选中商铺sku,
|
||||
selectedSpecImg: '', //选中的图片路径
|
||||
buyType: '', //用于存储促销,拼团等活动类型
|
||||
parentOrder: '', //父级拼团活动的数据 - 如果是团员则有数据
|
||||
formatList: [],
|
||||
currentSelected: [],
|
||||
skuList: '',
|
||||
isClose: false //是否可以点击遮罩关闭
|
||||
};
|
||||
},
|
||||
props: {
|
||||
wholesaleList: {
|
||||
type: null,
|
||||
default: false
|
||||
},
|
||||
buyMask: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
goodsDetail: {
|
||||
default: '',
|
||||
type: null
|
||||
},
|
||||
selectedSku: {
|
||||
default: '',
|
||||
type: null
|
||||
},
|
||||
goodsSpec: {
|
||||
default: '',
|
||||
type: null
|
||||
},
|
||||
addr: {
|
||||
default: '',
|
||||
type: null
|
||||
},
|
||||
pointDetail: {
|
||||
default: '',
|
||||
type: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wholesalePrice(key) {
|
||||
return this.wholesaleList.length
|
||||
? this.wholesaleList.map(item => {
|
||||
return item.price;
|
||||
})
|
||||
: [];
|
||||
},
|
||||
wholesaleNum(key) {
|
||||
return this.wholesaleList.length
|
||||
? this.wholesaleList.map(item => {
|
||||
return item.num;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
num(val) {
|
||||
|
||||
val == 0 ? this.num = 1 : ''
|
||||
if (val) {
|
||||
|
||||
//超过库存后修改回库存
|
||||
if (val > this.goodsDetail.quantity) {
|
||||
this.$nextTick(function() {
|
||||
this.num = this.goodsDetail.quantity;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
buyType: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.buyType = val;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectSkuList: {
|
||||
handler(val, oldval) {
|
||||
this.$emit('changed', val);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'goodsDetail.quantity': {
|
||||
handler(val) {
|
||||
if (val == 0) {
|
||||
uni.showToast({
|
||||
title: '商品已售罄',
|
||||
duration: 2000,
|
||||
icon: 'none'
|
||||
})
|
||||
this.num = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import * as API_trade from '@/api/trade.js'
|
||||
import setup from './popup.js'
|
||||
import uniNumberBox from '@/components/uni-number-box.vue'
|
||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||
import {
|
||||
isECoupon,
|
||||
isVirtualGoods,
|
||||
isAppointmentGoods,
|
||||
getECouponStock,
|
||||
getECouponMaxBuyNum,
|
||||
getAppointmentStock,
|
||||
getAppointmentMaxBuyNum,
|
||||
} from '@/utils/goodsType.js'
|
||||
|
||||
methods: {
|
||||
numCheck(val) {
|
||||
if (this.wholesaleList && this.wholesaleList.length > 0) {
|
||||
if (this.num <= this.wholesaleList[0].num) {
|
||||
uni.showToast({
|
||||
title: '批发商品购买数量不能小于起批数量!',
|
||||
duration: 2000,
|
||||
icon: 'none'
|
||||
});
|
||||
this.num = this.wholesaleList[0].num;
|
||||
}
|
||||
}
|
||||
},
|
||||
closeMask() {
|
||||
this.$emit('closeBuy', false);
|
||||
},
|
||||
|
||||
/**点击规格 */
|
||||
handleClickSpec(val, index, specValue) {
|
||||
this.currentSelected[index] = specValue.value;
|
||||
let selectedSkuId = this.goodsSpec.find(i => {
|
||||
let matched = true;
|
||||
let specValues = i.specValues.filter(j => j.specName !== 'images');
|
||||
for (let n = 0; n < specValues.length; n++) {
|
||||
if (specValues[n].specValue !== this.currentSelected[n]) {
|
||||
matched = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
if (selectedSkuId?.skuId) {
|
||||
this.$set(this.currentSelected, index, specValue.value);
|
||||
this.selectSkuList = {
|
||||
spec: {
|
||||
specName: val.name,
|
||||
specValue: specValue.value
|
||||
},
|
||||
data: this.goodsDetail
|
||||
};
|
||||
this.selectName = specValue.value;
|
||||
const props = withDefaults(defineProps<{
|
||||
buyMask?: boolean
|
||||
isGroup?: boolean
|
||||
goodsDetail?: any
|
||||
selectedSku?: any | null
|
||||
goodsSpec?: any | null
|
||||
addr?: any | null
|
||||
pointDetail?: any | null
|
||||
}>(), {
|
||||
buyMask: false,
|
||||
isGroup: false,
|
||||
goodsDetail: () => ({}),
|
||||
selectedSku: '',
|
||||
goodsSpec: '',
|
||||
addr: '',
|
||||
pointDetail: ''
|
||||
})
|
||||
|
||||
this.$emit('handleClickSku', {
|
||||
skuId: selectedSkuId.skuId,
|
||||
goodsId: this.goodsDetail.goodsId
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无该商品!',
|
||||
duration: 2000,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
const emit = defineEmits(['closeBuy', 'changed', 'handleClickSku', 'queryCart'])
|
||||
|
||||
/**
|
||||
* 直接购买
|
||||
*/
|
||||
buy(data) {
|
||||
API_trade.addToCart(data).then(res => {
|
||||
if (res.data.success) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/fillorder?way=${data.cartType}&addr=${''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
const num = ref(1)
|
||||
|
||||
/**
|
||||
* 添加到购物车或购买
|
||||
*/
|
||||
addToCartOrBuy(val) {
|
||||
if (!this.selectSkuList) {
|
||||
uni.showToast({
|
||||
title: '请选择规格商品',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
let data = {
|
||||
skuId: this.goodsDetail.id,
|
||||
num: this.num
|
||||
};
|
||||
const selectName = ref('')
|
||||
const selectSkuList = ref<any>('')
|
||||
const selectedSpecImg = ref('')
|
||||
const buyType = ref('')
|
||||
const parentOrder = ref<any>('')
|
||||
const formatList = ref<any[]>([])
|
||||
const currentSelected = ref<string[]>([])
|
||||
const skuList = ref('')
|
||||
|
||||
if (val == 'cart') {
|
||||
API_trade.addToCart(data).then(res => {
|
||||
if (res.data.code == 200) {
|
||||
uni.showToast({
|
||||
title: '商品已添加到购物车',
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
this.$emit('queryCart');
|
||||
this.closeMask();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 判断是否拼团商品
|
||||
if (this.buyType) {
|
||||
data.cartType = 'PINTUAN';
|
||||
} else if (this.goodsDetail.goodsType == 'VIRTUAL_GOODS') {
|
||||
data.cartType = 'VIRTUAL';
|
||||
} else {
|
||||
data.cartType = 'BUY_NOW';
|
||||
}
|
||||
|
||||
API_trade.addToCart(data).then(res => {
|
||||
if (res.data.code == 200) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/fillorder?way=${data.cartType}&addr=${this.addr.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
formatSku(list) {
|
||||
// 格式化数据
|
||||
let arr = [{}];
|
||||
|
||||
if (!Array.isArray(list)) {
|
||||
return false;
|
||||
}
|
||||
list.forEach((item, index) => {
|
||||
item.specValues.forEach((spec, specIndex) => {
|
||||
let name = spec.specName;
|
||||
let values = {
|
||||
value: spec.specValue,
|
||||
quantity: item.quantity,
|
||||
skuId: item.skuId
|
||||
};
|
||||
if (name === 'images') {
|
||||
return;
|
||||
}
|
||||
|
||||
arr.forEach((arrItem, arrIndex) => {
|
||||
if (
|
||||
arrItem.name == name &&
|
||||
arrItem.values &&
|
||||
!arrItem.values.find(i => {
|
||||
return i.value === values.value;
|
||||
})
|
||||
) {
|
||||
arrItem.values.push(values);
|
||||
}
|
||||
|
||||
let keys = arr.map(key => {
|
||||
return key.name;
|
||||
});
|
||||
if (!keys.includes(name)) {
|
||||
arr.push({
|
||||
name: name,
|
||||
values: [values]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
arr.shift();
|
||||
this.formatList = arr;
|
||||
|
||||
list.forEach(item => {
|
||||
// 默认选中
|
||||
if (item.skuId === this.goodsDetail.id) {
|
||||
item.specValues
|
||||
.filter(i => i.specName !== 'images')
|
||||
.forEach((value, _index) => {
|
||||
this.currentSelected[_index] = value.specValue;
|
||||
|
||||
this.selectName = value.specValue;
|
||||
|
||||
this.selectSkuList = {
|
||||
spec: value,
|
||||
data: this.goodsDetail
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.skuList = list;
|
||||
// console.log(" this.skuList", this.skuList)
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.formatSku(this.goodsSpec);
|
||||
|
||||
console.log("goodsDetail",this.goodsDetail)
|
||||
// E_COUPON 读 SKU quantity;预约商品 quantity 固定 0,库存走时段
|
||||
const availableStock = computed(() => {
|
||||
if (isECoupon(props.goodsDetail?.goodsType)) {
|
||||
return getECouponStock(props.goodsDetail)
|
||||
}
|
||||
};
|
||||
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||
return getAppointmentStock(props.goodsDetail)
|
||||
}
|
||||
return Number(props.goodsDetail?.quantity) || 0
|
||||
})
|
||||
|
||||
const maxBuyNum = computed(() => {
|
||||
if (isECoupon(props.goodsDetail?.goodsType)) {
|
||||
return getECouponMaxBuyNum(props.goodsDetail)
|
||||
}
|
||||
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||
return getAppointmentMaxBuyNum(props.goodsDetail)
|
||||
}
|
||||
return 999
|
||||
})
|
||||
|
||||
// 电子卡券/预约商品禁止加购
|
||||
const showAddToCart = computed(() => {
|
||||
return (
|
||||
buyType.value != 'PINTUAN' &&
|
||||
!isVirtualGoods(props.goodsDetail?.goodsType) &&
|
||||
!isECoupon(props.goodsDetail?.goodsType) &&
|
||||
!isAppointmentGoods(props.goodsDetail?.goodsType)
|
||||
)
|
||||
})
|
||||
|
||||
watch(num, (val) => {
|
||||
val == 0 ? num.value = 1 : ''
|
||||
if (val && val > maxBuyNum.value) {
|
||||
num.value = maxBuyNum.value
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.isGroup, (val) => {
|
||||
if (val) {
|
||||
buyType.value = 'PINTUAN'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(selectSkuList, (_val) => {
|
||||
emit('changed', selectSkuList.value)
|
||||
}, { deep: true })
|
||||
|
||||
watch(
|
||||
() => availableStock.value,
|
||||
(val) => {
|
||||
if (val == 0) {
|
||||
uni.showToast({
|
||||
title: '商品已售罄',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
num.value = 1
|
||||
} else if (num.value > maxBuyNum.value) {
|
||||
num.value = maxBuyNum.value
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
const closeMask = () => {
|
||||
emit('closeBuy', false)
|
||||
}
|
||||
|
||||
/**点击规格 */
|
||||
const handleClickSpec = (val: any, index: number, specValue: any) => {
|
||||
currentSelected.value[index] = specValue.value
|
||||
let selectedSkuId = props.goodsSpec.find((i: any) => {
|
||||
let matched = true
|
||||
let specValues = i.specValues.filter((j: any) => j.specName !== 'images')
|
||||
for (let n = 0; n < specValues.length; n++) {
|
||||
if (specValues[n].specValue !== currentSelected.value[n]) {
|
||||
matched = false
|
||||
return
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return i
|
||||
}
|
||||
})
|
||||
if (selectedSkuId?.skuId) {
|
||||
currentSelected.value[index] = specValue.value
|
||||
selectSkuList.value = {
|
||||
spec: {
|
||||
specName: val.name,
|
||||
specValue: specValue.value
|
||||
},
|
||||
data: props.goodsDetail
|
||||
}
|
||||
selectName.value = specValue.value
|
||||
|
||||
emit('handleClickSku', {
|
||||
skuId: selectedSkuId.skuId,
|
||||
goodsId: props.goodsDetail.goodsId
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无该商品!',
|
||||
duration: 2000,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动页/父组件直接调用购买(POINTS / KANJIA 等显式 cartType)
|
||||
*/
|
||||
const buy = (data: any) => {
|
||||
uni.hideLoading()
|
||||
API_trade.addToCart(data).then((res) => {
|
||||
if (res.data.success) {
|
||||
const way = data.cartType || 'BUY_NOW'
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/fillorder?way=${way}&addr=${''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value || ''))}`,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message || '购买失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
uni.hideLoading()
|
||||
})
|
||||
}
|
||||
|
||||
/** 解析 cartType:拼团 PINTUAN;核销 VIRTUAL;其余含 E_COUPON 为 BUY_NOW */
|
||||
function resolveCartType() {
|
||||
if (buyType.value === 'PINTUAN') {
|
||||
return 'PINTUAN'
|
||||
}
|
||||
if (isVirtualGoods(props.goodsDetail?.goodsType)) {
|
||||
return 'VIRTUAL'
|
||||
}
|
||||
return 'BUY_NOW'
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到购物车或购买
|
||||
*/
|
||||
const addToCartOrBuy = (val: string) => {
|
||||
if (!selectSkuList.value) {
|
||||
uni.showToast({
|
||||
title: '请选择规格商品',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
const data: Record<string, any> = {
|
||||
skuId: props.goodsDetail.id,
|
||||
num: num.value,
|
||||
}
|
||||
|
||||
if (val == 'cart') {
|
||||
API_trade.addToCart(data).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({
|
||||
title: '商品已添加到购物车',
|
||||
icon: 'none',
|
||||
})
|
||||
emit('queryCart')
|
||||
closeMask()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message || '加购失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (isAppointmentGoods(props.goodsDetail?.goodsType)) {
|
||||
const goodsId = props.goodsDetail.goodsId || ''
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/appointment/appointmentCheckout?skuId=${props.goodsDetail.id}&goodsId=${goodsId}&num=${num.value}`,
|
||||
})
|
||||
closeMask()
|
||||
return
|
||||
}
|
||||
data.cartType = resolveCartType()
|
||||
API_trade.addToCart(data).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/order/fillorder?way=${data.cartType}&addr=${props.addr?.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value || ''))}`,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message || '购买失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formatSku = (list: any[]) => {
|
||||
// 格式化数据
|
||||
let arr: any[] = [{}]
|
||||
|
||||
if (!Array.isArray(list)) {
|
||||
return false
|
||||
}
|
||||
list.forEach((_item, _index) => {
|
||||
let item = list[_index]
|
||||
item.specValues.forEach((spec: any, specIndex: number) => {
|
||||
let name = spec.specName
|
||||
let values = {
|
||||
value: spec.specValue,
|
||||
quantity: item.quantity,
|
||||
skuId: item.skuId
|
||||
}
|
||||
if (name === 'images') {
|
||||
return
|
||||
}
|
||||
|
||||
arr.forEach((arrItem, arrIndex) => {
|
||||
if (
|
||||
arrItem.name == name &&
|
||||
arrItem.values &&
|
||||
!arrItem.values.find((i: any) => {
|
||||
return i.value === values.value
|
||||
})
|
||||
) {
|
||||
arrItem.values.push(values)
|
||||
}
|
||||
|
||||
let keys = arr.map(key => {
|
||||
return key.name
|
||||
})
|
||||
if (!keys.includes(name)) {
|
||||
arr.push({
|
||||
name: name,
|
||||
values: [values]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
arr.shift()
|
||||
formatList.value = arr
|
||||
|
||||
list.forEach(item => {
|
||||
// 默认选中
|
||||
if (item.skuId === props.goodsDetail.id) {
|
||||
item.specValues
|
||||
.filter(i => i.specName !== 'images')
|
||||
.forEach((value, _index) => {
|
||||
currentSelected.value[_index] = value.specValue
|
||||
|
||||
selectName.value = value.specValue
|
||||
|
||||
selectSkuList.value = {
|
||||
spec: value,
|
||||
data: props.goodsDetail
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
skuList.value = list
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
formatSku(props.goodsSpec)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
buy,
|
||||
buyType,
|
||||
parentOrder,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './popup.scss';
|
||||
|
||||
@@ -428,18 +441,18 @@ export default {
|
||||
}
|
||||
|
||||
.buy {
|
||||
background-image: linear-gradient(135deg, #ffba0d, #ffc30d 69%, #ffcf0d);
|
||||
background-image: linear-gradient(135deg, $main-color, $light-color);
|
||||
box-shadow: 0 2px 6px 0 rgba(255, 65, 66, 0.2);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-image: linear-gradient(135deg, #f2140c, #f2270c 70%, #f24d0c);
|
||||
background-image: linear-gradient(91deg, $light-color 1%, $aider-light-color 99%);
|
||||
box-shadow: 0 2px 6px 0 rgba(255, 65, 66, 0.2);
|
||||
}
|
||||
|
||||
/deep/.u-icon-plus,
|
||||
.u-icon-minus,
|
||||
.u-icon-disabled {
|
||||
::v-deep .u-icon-plus,
|
||||
::v-deep .u-icon-minus,
|
||||
::v-deep .u-icon-disabled {
|
||||
height: 30rpx !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
@@ -457,7 +470,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .uni-scroll-view {
|
||||
::v-deep .uni-scroll-view {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
@@ -504,13 +517,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.wholesale-item {
|
||||
color: #999 !important;
|
||||
font-size: 24rpx;
|
||||
margin: 0 20rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-header {
|
||||
height: 200rpx;
|
||||
@@ -531,11 +537,6 @@ export default {
|
||||
color: $price-color;
|
||||
line-height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
|
||||
> * {
|
||||
color: $price-color;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.promotion-box {
|
||||
@@ -546,7 +547,7 @@ export default {
|
||||
color: #999;
|
||||
margin-left: 10rpx;
|
||||
|
||||
/deep/ span {
|
||||
.goods-price-bigshow {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
@@ -565,9 +566,6 @@ export default {
|
||||
|
||||
> .goods-check-skus-name {
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
### 说明 https://ext.dcloud.net.cn/plugin?id=3237
|
||||
@@ -1,211 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div class="index">
|
||||
<u-modal v-model="show" :show-title="false" :show-confirm-button="false" mask-close-able>
|
||||
<view class="slot-content">
|
||||
<image @click="downLoad()" class="img" :src="imgUrl" />
|
||||
<div class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas id="canvas" type="2d" style="width: 600px; height: 960px" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas canvas-id="canvas" id="canvas" style="width: 600px; height: 960px" />
|
||||
<!-- #endif -->
|
||||
</div>
|
||||
</view>
|
||||
</u-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// 引入绘制插件
|
||||
import DrawPoster from "@/js_sdk/u-draw-poster";
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
imgUrl: "", //绘制出来的图片路径
|
||||
show: false, //是否展示模态框
|
||||
dp: {}, //绘制的dp对象,用于存储绘制等一些方法。
|
||||
logo: require("@/pages/passport/static/logo-title.png"), //本地logo地址
|
||||
}),
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 父级传参的数据
|
||||
*/
|
||||
res: {
|
||||
type: null,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
onUnload() {},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 解决微信小程序中图片模糊问题
|
||||
*/
|
||||
// #ifdef MP-WEIXIN
|
||||
st2: (size) => size * 2,
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
st2: (size) => size,
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 保存图片
|
||||
*/
|
||||
downLoad() {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: this.imgUrl,
|
||||
success: function () {
|
||||
uni.showToast({
|
||||
title: "保存成功!",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
fail: function () {
|
||||
uni.showToast({
|
||||
title: "保存失败,请稍后重试!",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建canvas
|
||||
*/
|
||||
async init() {
|
||||
this.show = true;
|
||||
this.dp = await DrawPoster.build({
|
||||
selector: "canvas",
|
||||
componentThis: this,
|
||||
loading: true,
|
||||
debugging: true,
|
||||
});
|
||||
let dp = this.dp;
|
||||
// #ifdef MP-WEIXIN
|
||||
// 用于微信小程序中画布错乱问题
|
||||
dp.canvas.width = this.st2(600);
|
||||
dp.canvas.height = this.st2(960);
|
||||
// #endif
|
||||
this.draw(dp);
|
||||
},
|
||||
|
||||
async draw(dp) {
|
||||
const { width, height, background, title } = this.res.container;
|
||||
const { code, img, price } = this.res.bottom;
|
||||
|
||||
// /** 绘制背景 */
|
||||
await dp.draw((ctx) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.fillRoundRect(
|
||||
this.st2(0),
|
||||
this.st2(0),
|
||||
this.st2(width),
|
||||
this.st2(height),
|
||||
this.st2(12)
|
||||
);
|
||||
ctx.clip();
|
||||
});
|
||||
/** 绘制图片 */
|
||||
dp.draw(async (ctx) => {
|
||||
await Promise.all([
|
||||
// 绘制Logo
|
||||
ctx.drawImage(
|
||||
this.logo,
|
||||
this.st2(175),
|
||||
this.st2(0),
|
||||
this.st2(256),
|
||||
this.st2(144)
|
||||
),
|
||||
// 中间图片
|
||||
ctx.drawImage(
|
||||
img,
|
||||
this.st2(100),
|
||||
this.st2(150),
|
||||
this.st2(400),
|
||||
this.st2(400)
|
||||
),
|
||||
// 二维码
|
||||
ctx.drawImage(
|
||||
code,
|
||||
this.st2(39),
|
||||
this.st2(750),
|
||||
this.st2(150),
|
||||
this.st2(150)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
/** 绘制中间文字*/
|
||||
await dp.draw((ctx) => {
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.font = `bold ${this.st2(24)}px PingFang SC`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillWarpText({
|
||||
text: title,
|
||||
maxWidth: this.st2(500),
|
||||
x: this.st2(300),
|
||||
y: this.st2(600),
|
||||
layer: 1,
|
||||
});
|
||||
|
||||
ctx.fillStyle = "#ff3c2a";
|
||||
ctx.font = `${this.st2(38)}px PingFang SC`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(price, this.st2(300), this.st2(680));
|
||||
});
|
||||
|
||||
// /** 绘制底部文字 */
|
||||
await dp.draw((ctx) => {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = `${this.st2(24)}px PingFang SC`;
|
||||
ctx.fillText("长按图片,识别二维码", this.st2(200), this.st2(866));
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = `${this.st2(24)}px PingFang SC`;
|
||||
ctx.fillText("查看商品详情", this.st2(200), this.st2(900));
|
||||
});
|
||||
|
||||
this.imgUrl = await dp.createImagePath();
|
||||
|
||||
// console.log(posterImgUrl)
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.init();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page,
|
||||
.index {
|
||||
height: 100%;
|
||||
}
|
||||
.canvas-hide {
|
||||
/* 1 */
|
||||
position: fixed;
|
||||
right: 100vw;
|
||||
bottom: 100vh;
|
||||
/* 2 */
|
||||
z-index: -9999;
|
||||
/* 3 */
|
||||
opacity: 0;
|
||||
}
|
||||
.index {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba($color: grey, $alpha: 0.2);
|
||||
}
|
||||
|
||||
image {
|
||||
display: block;
|
||||
}
|
||||
.img {
|
||||
width: 600rpx;
|
||||
height: 960rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -79,182 +79,182 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import { getRegionsById } from "@/api/common.js"
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
let windowWidth = 0
|
||||
import { getRegionsById } from "@/api/common.js";
|
||||
export default {
|
||||
name: "UniCityNvue",
|
||||
props: {
|
||||
headTitle: {
|
||||
//标题
|
||||
type: String,
|
||||
default: "区域选择",
|
||||
},
|
||||
pickerSize: {
|
||||
// 使用多少个tab
|
||||
type: [String, String],
|
||||
default: "1",
|
||||
},
|
||||
provinceData: {
|
||||
// 默认的省市区id,如果不使用id的情况下则为[];
|
||||
type: Array,
|
||||
default: function () {
|
||||
return [];
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
headTitle?: string
|
||||
pickerSize?: string | string[]
|
||||
provinceData?: any[]
|
||||
}>(), {
|
||||
headTitle: "区域选择",
|
||||
pickerSize: "1",
|
||||
provinceData: () => [],
|
||||
})
|
||||
|
||||
const emit = defineEmits(['funcValue'])
|
||||
|
||||
const clearRightIcon = ref(true)
|
||||
const scrollLeft = ref(500)
|
||||
const scrollTop = ref(0)
|
||||
const enableScroll = ref(true)
|
||||
const tabCurrentIndex = ref(0)
|
||||
const tabbars = ref<any[]>(props.provinceData)
|
||||
const pickersize = ref(props.pickerSize)
|
||||
const showPicker = ref(false)
|
||||
|
||||
/**
|
||||
* 显示选择器
|
||||
*/
|
||||
const show = () => {
|
||||
showPicker.value = true
|
||||
if (tabbars.value[0].children.length == 0) {
|
||||
getRegionsById(0).then((res: any) => {
|
||||
tabbars.value[0].children = res.data.result
|
||||
})
|
||||
}
|
||||
|
||||
windowWidth = uni.getSystemInfoSync().windowWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭选择器
|
||||
*/
|
||||
const hide = () => {
|
||||
showPicker.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* tab切换
|
||||
*/
|
||||
const changeTab = (e: number) => {
|
||||
let index = e
|
||||
setScroll(index)
|
||||
//延迟300ms,等待swiper动画结束再修改tabbar
|
||||
tabCurrentIndex.value = index
|
||||
setTimeout(() => {
|
||||
getScroll("show" + index)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得元素的大小
|
||||
*/
|
||||
const getElSize = (id: string): Promise<any> => {
|
||||
return new Promise((res) => {
|
||||
let el = uni
|
||||
.createSelectorQuery()
|
||||
.in(instance!.proxy)
|
||||
.select("#" + id)
|
||||
el.fields(
|
||||
{
|
||||
size: true,
|
||||
scrollOffset: true,
|
||||
rect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clearRightIcon: true, //是否显示右侧关闭icon
|
||||
scrollLeft: 500, //顶部选项卡左滑距离
|
||||
scrollTop: 0, //默认滚动顶部为0
|
||||
enableScroll: true, //是否启用滚动
|
||||
tabCurrentIndex: 0, //当前选项卡索引
|
||||
tabbars: this.provinceData, //默认的省市区id
|
||||
pickersize: this.pickerSize, //多少个tab 推荐为4级
|
||||
showPicker: false, //显示选取器
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* 显示选择器
|
||||
*/
|
||||
show() {
|
||||
this.showPicker = true;
|
||||
if (this.tabbars[0].children.length == 0) {
|
||||
getRegionsById(0).then((res) => {
|
||||
this.tabbars[0].children = res.data.result;
|
||||
});
|
||||
(data: any) => {
|
||||
res(data)
|
||||
}
|
||||
).exec()
|
||||
})
|
||||
}
|
||||
|
||||
windowWidth = uni.getSystemInfoSync().windowWidth;
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭选择器
|
||||
*/
|
||||
hide() {
|
||||
this.showPicker = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* tab切换
|
||||
*/
|
||||
changeTab(e) {
|
||||
let index = e;
|
||||
this.setScroll(index);
|
||||
//延迟300ms,等待swiper动画结束再修改tabbar
|
||||
this.tabCurrentIndex = index;
|
||||
setTimeout(() => {
|
||||
this.getScroll("show" + index);
|
||||
}, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获得元素的大小
|
||||
*/
|
||||
getElSize(id) {
|
||||
return new Promise((res, rej) => {
|
||||
let el = uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select("#" + id);
|
||||
el.fields(
|
||||
{
|
||||
size: true,
|
||||
scrollOffset: true,
|
||||
rect: true,
|
||||
},
|
||||
(data) => {
|
||||
res(data);
|
||||
}
|
||||
).exec();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 点击城市后回调
|
||||
*/
|
||||
async changCity(index, item) {
|
||||
if (this.tabbars[index].id != item.id) {
|
||||
this.tabbars[index].localName = item.name;
|
||||
this.tabbars[index].id = item.id;
|
||||
this.tabbars[index].center = item.center
|
||||
if (index < this.tabbars.length - 1) {
|
||||
this.tabbars.splice(index + 1, this.tabbars.length - index - 1);
|
||||
}
|
||||
if (this.tabbars.length < this.pickersize) {
|
||||
let data = await getRegionsById(item.id);
|
||||
// 当前选项级为最后一级时回调,将选中的数据返回
|
||||
if (data.data.result.length == 0) {
|
||||
this.$emit("funcValue", this.tabbars);
|
||||
this.hide();
|
||||
} else {
|
||||
// 将新的数据填充进下一级
|
||||
var current = {
|
||||
localName: "请选择",
|
||||
id: "",
|
||||
children: data.data.result,
|
||||
|
||||
};
|
||||
this.tabbars.push(current);
|
||||
this.tabCurrentIndex++;
|
||||
|
||||
// 当前距离重新为最上面
|
||||
this.$set(this,'scrollTop',0)
|
||||
}
|
||||
/**
|
||||
* 点击城市后回调
|
||||
*/
|
||||
const changCity = async (index: number, item: any) => {
|
||||
if (tabbars.value[index].id != item.id) {
|
||||
tabbars.value[index].localName = item.name
|
||||
tabbars.value[index].id = item.id
|
||||
tabbars.value[index].center = item.center
|
||||
if (index < tabbars.value.length - 1) {
|
||||
tabbars.value.splice(index + 1, tabbars.value.length - index - 1)
|
||||
}
|
||||
if (tabbars.value.length < pickersize.value as number) {
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
mask: true
|
||||
})
|
||||
try {
|
||||
let data = await getRegionsById(item.id)
|
||||
uni.hideLoading()
|
||||
// 当前选项级为最后一级时回调,将选中的数据返回
|
||||
if (data.data.result.length == 0) {
|
||||
emit("funcValue", tabbars.value)
|
||||
hide()
|
||||
} else {
|
||||
this.$emit("funcValue", this.tabbars);
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 将新的数据填充进下一级
|
||||
var current = {
|
||||
localName: "请选择",
|
||||
id: "",
|
||||
children: data.data.result,
|
||||
}
|
||||
tabbars.value.push(current)
|
||||
tabCurrentIndex.value++
|
||||
|
||||
/**
|
||||
* 获取当前tab中滚动的距离
|
||||
*/
|
||||
async setScroll(index) {
|
||||
let width = 0;
|
||||
let nowWidth = 0;
|
||||
for (let i = 0; i <= index; i++) {
|
||||
let result = await this.getElSize("tab" + i);
|
||||
width += result.width;
|
||||
if (i === index) {
|
||||
nowWidth = result.width;
|
||||
// 当前距离重新为最上面
|
||||
scrollTop.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
if (width + nowWidth > windowWidth) {
|
||||
this.scrollLeft = width + nowWidth;
|
||||
} else {
|
||||
this.scrollLeft = 0;
|
||||
}
|
||||
},
|
||||
} else {
|
||||
emit("funcValue", tabbars.value)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前的滚动距离
|
||||
*/
|
||||
getScroll(id) {
|
||||
/**
|
||||
* 获取当前tab中滚动的距离
|
||||
*/
|
||||
const setScroll = async (index: number) => {
|
||||
let width = 0
|
||||
let nowWidth = 0
|
||||
for (let i = 0; i <= index; i++) {
|
||||
let result = await getElSize("tab" + i)
|
||||
width += result.width
|
||||
if (i === index) {
|
||||
nowWidth = result.width
|
||||
}
|
||||
}
|
||||
if (width + nowWidth > windowWidth) {
|
||||
scrollLeft.value = width + nowWidth
|
||||
} else {
|
||||
scrollLeft.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前的滚动距离
|
||||
*/
|
||||
const getScroll = (id: string) => {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(instance!.proxy)
|
||||
.select(".panel-scroll-box")
|
||||
.boundingClientRect((data: any) => {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select(".panel-scroll-box")
|
||||
.boundingClientRect((data) => {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select("#" + id)
|
||||
.boundingClientRect((res) => {
|
||||
if (res != undefined && res != null && res != "") {
|
||||
this.scrollTop = res.top - data.top;
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
.in(instance!.proxy)
|
||||
.select("#" + id)
|
||||
.boundingClientRect((res: any) => {
|
||||
if (res != undefined && res != null && res != "") {
|
||||
scrollTop.value = res.top - data.top
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
},
|
||||
};
|
||||
.exec()
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -381,12 +381,10 @@ export default {
|
||||
flex-direction: column;
|
||||
/* #endif */
|
||||
margin-right: 5px;
|
||||
margin-left: 40rpx;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.color {
|
||||
color: $light-color;
|
||||
}
|
||||
icon {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,4 +8,11 @@
|
||||
| `type` | 商品展示类型 oneColumns twoColumns ,默认展示一行两列商品 | String | 否 |
|
||||
| `storeName` | 是否展示店铺名称,默认展示 | Boolean | 否 |
|
||||
| `keywords` | 高亮展示搜索内容 | String | 否 |
|
||||
| `showFields` | 商品卡片字段开关(Object,null=现状渲染全量字段)。键:`subTitle` 副标题、`spec` 规格摘要(取 attrList 前 2 项)、`tags` 促销标签、`selfTag` 自营、`price` 价格、`linePrice` 划线价(仅 marketPrice>price 时显示)、`sales` 已售/评论、`cart` 购物车按钮。新增字段(副标题/规格/划线价/购物车)仅在 showFields 提供时才会渲染;目前仅 oneColumns 布局支持开关 | Object | 否 |
|
||||
|
||||
### EVENTS 事件说明
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ------ | ---- | -------- |
|
||||
| `addCart` | 点击购物车按钮(showFields.cart 开启时渲染) | 当前商品对象 |
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<view>
|
||||
<!-- 一行两列商品展示 -->
|
||||
<view class="goods-list" v-if="type == 'twoColumns'">
|
||||
<view v-for="(item, index) in res" :key="index" class="goods-item">
|
||||
@@ -10,7 +10,7 @@
|
||||
height="330rpx"
|
||||
mode="aspectFit"
|
||||
>
|
||||
<u-loading slot="loading"></u-loading>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-detail">
|
||||
@@ -23,9 +23,9 @@
|
||||
<div class="price" v-if="item.price != undefined">
|
||||
¥<span
|
||||
>{{
|
||||
$options.filters.goodsFormatPrice(item.price)[0]
|
||||
goodsFormatPrice(item.price)[0]
|
||||
}} </span
|
||||
>.{{ $options.filters.goodsFormatPrice(item.price)[1] }}
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</div>
|
||||
</view>
|
||||
<div class="count-config" @click="navigateToDetailPage(item)">
|
||||
@@ -57,7 +57,7 @@
|
||||
</view>
|
||||
<!-- 一行一列商品展示 -->
|
||||
<div v-if="type == 'oneColumns'">
|
||||
<div v-for="(item, index) in res" :key="index" class="goods-row">
|
||||
div v-for="(item, index) in res" :key="index" class="goods-row">
|
||||
<div class="flex goods-col">
|
||||
<div class="goods-img" @click="navigateToDetailPage(item)">
|
||||
<u-image
|
||||
@@ -67,7 +67,7 @@
|
||||
mode="aspectFit"
|
||||
:src="item.goodsImage || item.thumbnail"
|
||||
>
|
||||
<u-loading slot="loading"></u-loading>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
@@ -77,14 +77,11 @@
|
||||
<view class="price-box" @click="navigateToDetailPage(item)">
|
||||
<div class="price" v-if="item.price != undefined">
|
||||
¥<span
|
||||
>{{ $options.filters.goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ $options.filters.goodsFormatPrice(item.price)[1] }}
|
||||
>{{ goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</div>
|
||||
</view>
|
||||
<div class="promotion" @click="navigateToDetailPage(item)">
|
||||
<div v-if="item.salesModel == 'WHOLESALE'">
|
||||
<span>批</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(promotionItem, promotionIndex) in getPromotion(item)"
|
||||
:key="promotionIndex"
|
||||
@@ -112,41 +109,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonTpl from "@/components/m-goods-list/common";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lightColor: this.$mainColor,
|
||||
};
|
||||
},
|
||||
mixins: [commonTpl],
|
||||
props: {
|
||||
// 展示的类型
|
||||
type:{
|
||||
type:String,
|
||||
default:"oneColumns"
|
||||
},
|
||||
// 遍历的数据
|
||||
res: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 跳转到商品详情
|
||||
navigateToDetailPage(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { useGoodsListCommon } from './common'
|
||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||
|
||||
const { lightSearchStr, getPromotion, navigateToDetailPage, navigateToStoreDetailPage } = useGoodsListCommon()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
type?: string
|
||||
res?: any[]
|
||||
storeName?: boolean
|
||||
keyword?: string | null
|
||||
}>(), {
|
||||
type: 'oneColumns',
|
||||
res: () => [],
|
||||
storeName: true,
|
||||
keyword: ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -268,7 +250,7 @@ export default {
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
|
||||
/deep/ span:nth-of-type(1) {
|
||||
::v-deep span:nth-of-type(1) {
|
||||
font-size: 38rpx;
|
||||
}
|
||||
}
|
||||
|
||||
90
components/m-goods-list/common.ts
Normal file
90
components/m-goods-list/common.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useStore } from '@/store'
|
||||
|
||||
/**
|
||||
* 商品列表公共逻辑(原 common.vue mixin)
|
||||
* 提供高亮搜索、unicode 转换、促销标签、导航跳转等通用方法
|
||||
*/
|
||||
export function useGoodsListCommon() {
|
||||
const store = useStore()
|
||||
|
||||
const lightColor = store.getters.lightColor
|
||||
|
||||
/**
|
||||
* 高亮显示搜索内容
|
||||
*/
|
||||
const lightSearchStr = (keyword: string, str: string) => {
|
||||
if (!keyword) {
|
||||
return str
|
||||
} else {
|
||||
let unicodes = ''
|
||||
for (let i of Array.from(keyword)) {
|
||||
unicodes += unicode(i) + "|"
|
||||
}
|
||||
const rule = '(' + unicodes + ')'
|
||||
const reg = new RegExp(rule, 'gi')
|
||||
return str ? str.replace(reg, (matchValue) =>
|
||||
`<span style="color:${lightColor}">${matchValue}</span>`
|
||||
) : ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 unicode
|
||||
*/
|
||||
const unicode = (str: string) => {
|
||||
var value = ''
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
value += '\\u' + leftZero4(parseInt(str.charCodeAt(i).toString(16)))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const leftZero4 = (str: string) => {
|
||||
if (str != null && str != '' && str != 'undefined') {
|
||||
if (str.length == 2) {
|
||||
return '00' + str
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据去重 只显示一次 减免 劵等
|
||||
*/
|
||||
const getPromotion = (item: any) => {
|
||||
if (item.promotionMap) {
|
||||
let array: string[] = []
|
||||
Object.keys(item.promotionMap).forEach((child) => {
|
||||
if (!array.includes(child.split("-")[0])) {
|
||||
array.push(child.split("-")[0])
|
||||
}
|
||||
})
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到商品详情
|
||||
*/
|
||||
const navigateToDetailPage = (item: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到店铺详情
|
||||
*/
|
||||
const navigateToStoreDetailPage = (item: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/shopPage?id=${item.storeId}`,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
lightSearchStr,
|
||||
getPromotion,
|
||||
navigateToDetailPage,
|
||||
navigateToStoreDetailPage,
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
// 高亮显示搜索内容
|
||||
lightSearchStr(keyword, str) {
|
||||
if (!keyword) {
|
||||
return str
|
||||
} else {
|
||||
let unicodes = '';
|
||||
for (let i of Array.from(keyword)) {
|
||||
unicodes += this.unicode(i) + "|"
|
||||
}
|
||||
const rule = '(' + unicodes + ')'
|
||||
const reg = new RegExp(rule, 'gi');
|
||||
return str ? str.replace(reg, matchValue =>
|
||||
`<span style="color:${this.lightColor}">${matchValue}</span>`
|
||||
) : ''
|
||||
}
|
||||
},
|
||||
// 转换为unicode
|
||||
unicode(str) {
|
||||
var value = '';
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
value += '\\u' + this.left_zero_4(parseInt(str.charCodeAt(i)).toString(16));
|
||||
}
|
||||
return value;
|
||||
},
|
||||
left_zero_4(str) {
|
||||
if (str != null && str != '' && str != 'undefined') {
|
||||
if (str.length == 2) {
|
||||
return '00' + str;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
},
|
||||
// 数据去重一下 只显示一次 减免 劵 什么的
|
||||
getPromotion(item) {
|
||||
if (item.promotionMap) {
|
||||
let array = [];
|
||||
Object.keys(item.promotionMap).forEach((child) => {
|
||||
if (!array.includes(child.split("-")[0])) {
|
||||
array.push(child.split("-")[0]);
|
||||
}
|
||||
});
|
||||
return array;
|
||||
}
|
||||
},
|
||||
// 跳转到商品详情
|
||||
navigateToDetailPage(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
});
|
||||
},
|
||||
// 跳转地址
|
||||
navigateToStoreDetailPage(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/shopPage?id=${item.storeId}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 一行两列商品展示 -->
|
||||
<view class="goods-list" v-if="type == 'twoColumns'">
|
||||
<view class="goods-list" :class="{ 'goods-list--embedded': !tabBarGap }" v-if="type == 'twoColumns'">
|
||||
<view v-for="(item, index) in res" :key="index" class="goods-item">
|
||||
<view class="image-wrapper" @click="navigateToDetailPage(item)">
|
||||
<u-image :src="item.thumbnail" width="100%" height='330rpx' mode="aspectFit">
|
||||
<u-loading slot="loading"></u-loading>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-detail">
|
||||
@@ -15,19 +15,16 @@
|
||||
</div>
|
||||
<view class="price-box" @click="navigateToDetailPage(item)">
|
||||
<div class="price" v-if="item.price!=undefined">
|
||||
¥<span>{{ $options.filters.goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
$options.filters.goodsFormatPrice(item.price )[1]
|
||||
¥<span class="price-int">{{ goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
goodsFormatPrice(item.price )[1]
|
||||
}}
|
||||
</div>
|
||||
</view>
|
||||
<div class="promotion" @click="navigateToDetailPage(item)">
|
||||
<div v-if="item.salesModel == 'WHOLESALE'">
|
||||
<span>批</span>
|
||||
</div>
|
||||
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
||||
<span v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
||||
<span v-if="promotionItem.indexOf('FULL_DISCOUNT') != -1">满减</span>
|
||||
<span v-if="promotionItem.indexOf('SECKILL') != -1">秒杀</span>
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('FULL_DISCOUNT') != -1">满减</span>
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('SECKILL') != -1">秒杀</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="count-config" @click="navigateToDetailPage(item)">
|
||||
@@ -35,175 +32,114 @@
|
||||
<span>{{ item.commentNum || "0" }}条评论</span>
|
||||
</div>
|
||||
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
||||
<div class="text-hidden">
|
||||
<u-tag style="margin-right: 10rpx" size="mini" mode="dark" v-if="item.selfOperated"
|
||||
text="自营" type="error" />
|
||||
<span>{{ item.storeName || "暂无" }}</span>
|
||||
</div>
|
||||
<span>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</span>
|
||||
<view class="store-name-row">
|
||||
<text class="self-tag" v-if="item.selfOperated">自营</text>
|
||||
<text class="store-name-text">{{ item.storeName || "暂无" }}</text>
|
||||
<text class="to-store">进店</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="12" color="#c5c5c5"></u-icon>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 一行一列商品展示 -->
|
||||
<div v-if="type == 'oneColumns'" class="goods-one-row">
|
||||
<div v-if="type == 'oneColumns'" class="goods-one-row" :class="{ 'goods-one-row--embedded': !tabBarGap }">
|
||||
<div v-for="(item, index) in res" :key="index" class="goods-row">
|
||||
<div class="flex goods-col">
|
||||
<div class="goods-img" @click="navigateToDetailPage(item)">
|
||||
<u-image width="230rpx" mode="aspectFit" border-radius='16' height="230rpx" :src="item.thumbnail">
|
||||
<u-loading slot="loading"></u-loading>
|
||||
<u-image :width="!tabBarGap ? '170rpx' : '230rpx'" mode="aspectFit" :border-radius="!tabBarGap ? '12' : '16'" :height="!tabBarGap ? '170rpx' : '230rpx'" :src="item.thumbnail">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="title clamp3" @click="navigateToDetailPage(item)">{{ item.goodsName }}</div>
|
||||
<div class="title clamp2" @click="navigateToDetailPage(item)">
|
||||
<text class="self-tag" v-if="(!storeName || flag('selfTag')) && item.selfOperated">自营</text>
|
||||
{{ item.goodsName }}
|
||||
</div>
|
||||
<div class="selling-point" v-if="extra('subTitle') && item.sellingPoint">{{ item.sellingPoint }}</div>
|
||||
<div class="spec-summary" v-if="extra('spec') && specSummary(item)">{{ specSummary(item) }}</div>
|
||||
<div class="promotion" v-if="flag('tags')" @click="navigateToDetailPage(item)">
|
||||
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('FULL_DISCOUNT') != -1">满减</span>
|
||||
<span class="promotion-tag" v-if="promotionItem.indexOf('SECKILL') != -1">秒杀</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow: hidden" v-if="flag('sales')" @click="navigateToDetailPage(item)" class="count-config">
|
||||
<span style="float: left; font-size: 22rpx">已售 {{ item.buyCount || '0' }}</span>
|
||||
<span style="float: right; font-size: 22rpx" v-if="item.commentNum">{{ item.commentNum }}条评论</span>
|
||||
</div>
|
||||
<view class="price-box" @click="navigateToDetailPage(item)">
|
||||
<div class="price" v-if="item.price!=undefined">
|
||||
¥<span>{{ $options.filters.goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
$options.filters.goodsFormatPrice(item.price )[1]
|
||||
<div class="price" v-if="flag('price') && item.price!=undefined">
|
||||
¥<span class="price-int">{{ goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
goodsFormatPrice(item.price )[1]
|
||||
}}
|
||||
</div>
|
||||
<div class="market-price" v-if="extra('linePrice') && hasMarketPrice(item)">¥{{ item.marketPrice }}</div>
|
||||
<div class="cart-btn" v-if="extra('cart')" @click.stop="emit('addCart', item)">
|
||||
<u-icon name="shopping-cart" size="16" :color="mainColor"></u-icon>
|
||||
</div>
|
||||
</view>
|
||||
<div class="promotion" @click="navigateToDetailPage(item)">
|
||||
<div v-if="item.salesModel == 'WHOLESALE'">
|
||||
<span>批</span>
|
||||
</div>
|
||||
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
||||
<span v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
||||
<span v-if="promotionItem.indexOf('FULL_DISCOUNT') != -1">满减</span>
|
||||
<span v-if="promotionItem.indexOf('SECKILL') != -1">秒杀</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow: hidden" @click="navigateToDetailPage(item)" class="count-config">
|
||||
<span style="float: left; font-size: 22rpx">已售 {{ item.buyCount || '0' }}</span>
|
||||
<span style="float: right; font-size: 22rpx">{{ item.commentNum || '0' }}条评论</span>
|
||||
</div>
|
||||
<div style="overflow: hidden" @click="navigateToStoreDetailPage(item)" class="count-config">
|
||||
<div class="text-hidden" v-if="storeName">
|
||||
<u-tag style="margin-right: 10rpx" size="mini" mode="dark" v-if="item.selfOperated"
|
||||
text="自营" type="error" />
|
||||
<span class="line1-store-name">{{ item.storeName }}</span>
|
||||
<span class="to-store">进店<u-icon size="24" name="arrow-right" color="#666"></u-icon>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<u-icon name="arrow-right" color="#c5c5c5"></u-icon>
|
||||
</span>
|
||||
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
||||
<view class="store-name-row">
|
||||
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
|
||||
<text class="store-name-text">{{ item.storeName }}</text>
|
||||
<text class="to-store">进店</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="12" color="#c5c5c5"></u-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonTpl from '@/components/m-goods-list/common'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lightColor: this.$mainColor
|
||||
}
|
||||
},
|
||||
mixins: [commonTpl],
|
||||
|
||||
props: {
|
||||
// 遍历的数据
|
||||
res: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// 一行两列还是一行一列显示
|
||||
type: {
|
||||
type: String,
|
||||
default: 'twoColumns',
|
||||
validator() {
|
||||
return ['twoColumns', 'oneColumns']
|
||||
}
|
||||
},
|
||||
storeName: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
keyword: {
|
||||
type: null,
|
||||
default: ''
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { useGoodsListCommon } from './common'
|
||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||
|
||||
},
|
||||
watch: {
|
||||
keyword(val) {
|
||||
if (val) {
|
||||
this.lightSearchStr(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
const store = useStore()
|
||||
const mainColor = computed(() => (store && store.getters && store.getters.mainColor) || '#ff3c2a')
|
||||
|
||||
// 高亮显示搜索内容
|
||||
lightSearchStr(keyword, str) {
|
||||
if (!keyword) {
|
||||
return str
|
||||
} else {
|
||||
let unicodes = '';
|
||||
for (let i of Array.from(keyword)) {
|
||||
unicodes += this.unicode(i) + "|"
|
||||
}
|
||||
const rule = '(' + unicodes + ')'
|
||||
const reg = new RegExp(rule, 'gi');
|
||||
return str ? str.replace(reg, matchValue =>
|
||||
`<span style="color:${this.lightColor}">${matchValue}</span>`
|
||||
) : ''
|
||||
}
|
||||
},
|
||||
// 转换为unicode
|
||||
unicode(str) {
|
||||
var value = '';
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
value += '\\u' + this.left_zero_4(parseInt(str.charCodeAt(i)).toString(16));
|
||||
}
|
||||
return value;
|
||||
},
|
||||
left_zero_4(str) {
|
||||
if (str != null && str != '' && str != 'undefined') {
|
||||
if (str.length == 2) {
|
||||
return '00' + str;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
},
|
||||
// 数据去重一下 只显示一次 减免 劵 什么的
|
||||
getPromotion(item) {
|
||||
if (item ? item.promotionMap : item.promotionMap) {
|
||||
const fieldList = item ? item.promotionMap : item.promotionMap
|
||||
let array = [];
|
||||
Object.keys(fieldList).forEach((child) => {
|
||||
if (!array.includes(child.split("-")[0])) {
|
||||
array.push(child.split("-")[0]);
|
||||
}
|
||||
});
|
||||
return array;
|
||||
}
|
||||
},
|
||||
// 跳转到商品详情
|
||||
navigateToDetailPage(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
});
|
||||
},
|
||||
// 跳转地址
|
||||
navigateToStoreDetailPage(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/shopPage?id=${item.storeId}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
const { lightSearchStr, getPromotion, navigateToDetailPage, navigateToStoreDetailPage } = useGoodsListCommon()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
res?: any[]
|
||||
type?: string
|
||||
storeName?: boolean
|
||||
tabBarGap?: boolean
|
||||
keyword?: string | null
|
||||
showFields?: Record<string, boolean> | null
|
||||
}>(), {
|
||||
res: () => [],
|
||||
type: 'twoColumns',
|
||||
storeName: true,
|
||||
tabBarGap: true,
|
||||
keyword: '',
|
||||
showFields: null
|
||||
})
|
||||
|
||||
const emit = defineEmits(['addCart'])
|
||||
|
||||
// 存量字段(促销/销量/自营/价格):showFields 为 null 时维持现状渲染
|
||||
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false
|
||||
// 新增字段(副标题/规格/划线价/购物车按钮):仅 showFields 提供时可渲染
|
||||
const extra = (key: string) => props.showFields != null && props.showFields[key] !== false
|
||||
const hasMarketPrice = (item: any) =>
|
||||
item.marketPrice != null && item.price != null && item.marketPrice > item.price
|
||||
const specSummary = (item: any) =>
|
||||
Array.isArray(item.attrList)
|
||||
? item.attrList.slice(0, 2).map((a: any) => `${a.name}: ${a.value}`).join(' · ')
|
||||
: ''
|
||||
|
||||
watch(() => props.keyword, (val) => {
|
||||
if (val) {
|
||||
lightSearchStr(val as string, '')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@@ -214,23 +150,25 @@
|
||||
.goods-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin: 10rpx 20rpx 284rpx;
|
||||
width: 100%;
|
||||
|
||||
width: calc(100% - 40rpx);
|
||||
box-sizing: border-box;
|
||||
|
||||
&.goods-list--embedded {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
>.goods-item {
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
border-radius: 16rpx;
|
||||
flex-direction: column;
|
||||
width: calc(50% - 30rpx);
|
||||
width: calc(50% - 10rpx);
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
height: 330rpx;
|
||||
@@ -239,41 +177,209 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.count-config,
|
||||
.store-seller-name {
|
||||
font-size: $font-sm;
|
||||
}
|
||||
|
||||
.text-hidden {
|
||||
|
||||
.store-seller-name {
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.store-name-row {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-name-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.to-store {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.self-tag {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.count-config {
|
||||
padding: 5rpx 0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
font-size: 24rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.goods-one-row--embedded {
|
||||
.goods-row {
|
||||
padding: 20rpx 16rpx;
|
||||
border-bottom: 1rpx solid #f6f6f6;
|
||||
background: #ffffff;
|
||||
|
||||
>.goods-col {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
>.goods-img {
|
||||
flex: 0 0 170rpx;
|
||||
width: 170rpx;
|
||||
height: 170rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
>.goods-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0 0 0 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
>.title {
|
||||
font-size: 26rpx;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
max-height: 74rpx;
|
||||
padding: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.self-tag {
|
||||
display: inline-block;
|
||||
margin-right: 8rpx;
|
||||
padding: 1rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.selling-point {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.3;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spec-summary {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.3;
|
||||
margin-top: 2rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.count-config {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 8rpx;
|
||||
|
||||
.price {
|
||||
font-size: 22rpx;
|
||||
color: $main-color;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
|
||||
.price-int {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.market-price {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
text-decoration: line-through;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid $main-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 60, 42, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-row {
|
||||
background: #fff;
|
||||
padding: 16rpx;
|
||||
|
||||
|
||||
>.goods-col {
|
||||
display: flex;
|
||||
|
||||
|
||||
>.goods-img {
|
||||
overflow: hidden;
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
|
||||
>.goods-detail {
|
||||
flex: 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.goods-detail {
|
||||
margin: 0 20rpx;
|
||||
|
||||
|
||||
>.title {
|
||||
font-size: $font-base;
|
||||
color: $font-color-dark;
|
||||
@@ -285,29 +391,106 @@
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.promotion {
|
||||
|
||||
.selling-point {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spec-summary {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.market-price {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid $main-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 60, 42, 0.12);
|
||||
}
|
||||
|
||||
.promotion {
|
||||
margin-top: 4rpx;
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
span {
|
||||
font-size: 24rpx;
|
||||
color: $light-color;
|
||||
margin-right: 10rpx;
|
||||
padding: 0 4rpx;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.promotion-tag {
|
||||
font-size: 24rpx;
|
||||
color: $light-color;
|
||||
margin-right: 10rpx;
|
||||
padding: 0 4rpx;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.store-seller-name {
|
||||
color: #666;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
|
||||
.store-name-row {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-name-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.to-store {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.self-tag {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
background: $main-color;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.count-config {
|
||||
padding: 5rpx 0;
|
||||
color: #666;
|
||||
@@ -315,7 +498,7 @@
|
||||
font-size: 24rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
>.price-box {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
@@ -324,18 +507,17 @@
|
||||
padding-right: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: $font-color-light;
|
||||
|
||||
|
||||
>.price {
|
||||
font-size: 26rpx;
|
||||
line-height: 1;
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
|
||||
/deep/ span:nth-of-type(1) {
|
||||
|
||||
.price-int {
|
||||
font-size: 38rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,170 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(item, index) in res" :key="index" class="goods-row" @click="navigateToDetailPage(item)">
|
||||
<div class="flex goods-col">
|
||||
<div class="goods-img">
|
||||
<u-image width="230rpx" mode="aspectFit" border-radius='16' height="230rpx" :src="item.goodsImage || item.thumbnail">
|
||||
<u-loading slot="loading"></u-loading>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="title clamp3">{{ item.goodsName }}</div>
|
||||
<div class='flex flex-a-c flex-j-sb'>
|
||||
<view class="price-box">
|
||||
<!-- 秒杀 / 拼团 -->
|
||||
<div class="price" v-if="!type && item.price!=undefined">
|
||||
¥<span>{{ $options.filters.goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
$options.filters.goodsFormatPrice(item.price )[1]
|
||||
}}
|
||||
</div>
|
||||
<!-- 砍价 -->
|
||||
<div class="price" v-if="type && item.purchasePrice!=undefined">
|
||||
最低:
|
||||
¥<span>{{ $options.filters.goodsFormatPrice(item.purchasePrice )[0] }} </span>.{{
|
||||
$options.filters.goodsFormatPrice(item.purchasePrice )[1]
|
||||
}}
|
||||
</div>
|
||||
<!-- 兜底策略如果金额是0 -->
|
||||
<div class="price" v-if="!item.price && !type">
|
||||
¥<span>0 </span>.00
|
||||
</div>
|
||||
</view>
|
||||
<div>
|
||||
<image class='buy' :src="buy"></image>
|
||||
</div>
|
||||
</div>
|
||||
<div class='count-config' v-if="!type">
|
||||
<span>即将恢复{{ item.originalPrice}}元</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonTpl from '@/components/m-goods-list/common'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lightColor: this.$mainColor,
|
||||
buy: require('@/static/buy.png')
|
||||
}
|
||||
},
|
||||
mixins: [commonTpl],
|
||||
props: {
|
||||
// 遍历的数据
|
||||
res: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
type:{
|
||||
type:null,
|
||||
default:""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 跳转到商品详情
|
||||
navigateToDetailPage(item) {
|
||||
if(this.type == 'kanJia'){
|
||||
uni.navigateTo({
|
||||
url: `/pages/promotion/bargain/detail?id=${item.id}`,
|
||||
});
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.skuId}&goodsId=${item.goodsId}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.buy {
|
||||
width: 152rpx;
|
||||
height: 108rpx;
|
||||
}
|
||||
.flex-j-sb {
|
||||
width: 100%;
|
||||
}
|
||||
.goods-row {
|
||||
background: #fff;
|
||||
padding: 16rpx;
|
||||
>.goods-col {
|
||||
display: flex;
|
||||
>.goods-img {
|
||||
overflow: hidden;
|
||||
flex: 4;
|
||||
}
|
||||
>.goods-detail {
|
||||
flex: 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
.goods-detail {
|
||||
margin: 0 20rpx;
|
||||
>.title {
|
||||
font-size: $font-base;
|
||||
color: $font-color-dark;
|
||||
line-height: 1.5;
|
||||
height: 86rpx;
|
||||
padding: 10rpx 0 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.promotion {
|
||||
margin-top: 4rpx;
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
span {
|
||||
font-size: 24rpx;
|
||||
color: $light-color;
|
||||
margin-right: 10rpx;
|
||||
padding: 0 4rpx;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count-config {
|
||||
padding: 5rpx 0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
font-size: 24rpx;
|
||||
letter-spacing:2rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.price-box {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: $font-color-light;
|
||||
|
||||
>.price {
|
||||
font-size: 26rpx;
|
||||
line-height: 1;
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
|
||||
/deep/ span:nth-of-type(1) {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(item, index) in res" :key="index" class="goods-row" @click="onItemClick(item)">
|
||||
<div class="flex goods-col">
|
||||
<div class="goods-img">
|
||||
<u-image width="230rpx" mode="aspectFit" border-radius='16' height="230rpx" :src="item.goodsImage || item.thumbnail">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="title clamp3">{{ item.goodsName }}</div>
|
||||
<div class='flex flex-a-c flex-j-sb'>
|
||||
<view class="price-box">
|
||||
<!-- 秒杀 / 拼团 -->
|
||||
<div class="price" v-if="!type && item.price!=undefined">
|
||||
¥<span class="price-int">{{ goodsFormatPrice(item.price )[0] }} </span>.{{
|
||||
goodsFormatPrice(item.price )[1]
|
||||
}}
|
||||
</div>
|
||||
<!-- 砍价 -->
|
||||
<div class="price" v-if="type && item.purchasePrice!=undefined">
|
||||
最低:
|
||||
¥<span class="price-int">{{ goodsFormatPrice(item.purchasePrice )[0] }} </span>.{{
|
||||
goodsFormatPrice(item.purchasePrice )[1]
|
||||
}}
|
||||
</div>
|
||||
<!-- 兜底策略如果金额是0 -->
|
||||
<div class="price" v-if="!item.price && !type">
|
||||
¥<span class="price-int">0 </span>.00
|
||||
</div>
|
||||
</view>
|
||||
<div>
|
||||
<image class="buy" src="/static/buy.png"></image>
|
||||
</div>
|
||||
</div>
|
||||
<div class='count-config' v-if="!type">
|
||||
<text class="count-config-text">即将恢复{{ item.originalPrice}}元</text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
res?: any[]
|
||||
type?: string | null
|
||||
}>(), {
|
||||
res: () => [],
|
||||
type: ''
|
||||
})
|
||||
|
||||
// 促销列表跳转:拼团/秒杀 PromotionGoods 用 skuId;砍价走活动详情
|
||||
function onItemClick(item: any) {
|
||||
if (props.type == 'kanJia') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/promotion/bargain/detail?id=${item.id}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const skuId = item.skuId || item.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${skuId}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.buy {
|
||||
width: 152rpx;
|
||||
height: 108rpx;
|
||||
}
|
||||
.flex-j-sb {
|
||||
width: 100%;
|
||||
}
|
||||
.goods-row {
|
||||
background: #fff;
|
||||
padding: 16rpx;
|
||||
>.goods-col {
|
||||
display: flex;
|
||||
>.goods-img {
|
||||
overflow: hidden;
|
||||
flex: 4;
|
||||
}
|
||||
>.goods-detail {
|
||||
flex: 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
.goods-detail {
|
||||
margin: 0 20rpx;
|
||||
>.title {
|
||||
font-size: $font-base;
|
||||
color: $font-color-dark;
|
||||
line-height: 1.5;
|
||||
height: 86rpx;
|
||||
padding: 10rpx 0 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.count-config {
|
||||
padding: 5rpx 0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
font-size: 24rpx;
|
||||
letter-spacing:2rpx;
|
||||
padding-left: 10rpx;
|
||||
|
||||
.count-config-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.price-box {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: $font-color-light;
|
||||
|
||||
>.price {
|
||||
font-size: 26rpx;
|
||||
line-height: 1;
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
|
||||
.price-int {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,73 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="goods-recommend">{{title ? `--${title}-- `:''}}</div>
|
||||
<view>
|
||||
<view class="goods-recommend">{{title ? `--${title}-- `:''}}</view>
|
||||
<goodsTemplate :res='goodsList' />
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import goodsTemplate from '@/components/m-goods-list/list'
|
||||
import { getGoodsList } from "@/api/goods.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
goodsList: [],
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
pageSize: {
|
||||
type: null,
|
||||
default: 12,
|
||||
},
|
||||
categoryId: {
|
||||
type: null,
|
||||
default: "",
|
||||
},
|
||||
storeId: {
|
||||
type: null,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
components:{goodsTemplate},
|
||||
mounted() {
|
||||
this.initGoods();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 初始化商品
|
||||
*/
|
||||
async initGoods() {
|
||||
let submit = JSON.parse(
|
||||
JSON.stringify(
|
||||
Object.assign(this.params, {
|
||||
pageSize: this.pageSize,
|
||||
categoryId: this.categoryId,
|
||||
storeId: this.storeId,
|
||||
})
|
||||
)
|
||||
);
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import goodsTemplate from '@/components/m-goods-list/list.vue'
|
||||
import { getGoodsList } from "@/api/goods.js"
|
||||
|
||||
Object.keys(submit).map((key) => {
|
||||
if (!submit[key] || submit[key].length == 0) {
|
||||
delete submit[key];
|
||||
}
|
||||
});
|
||||
let goodsList = await getGoodsList(submit);
|
||||
this.goodsList.push(...goodsList.data.result.records);
|
||||
},
|
||||
handleClick(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string
|
||||
pageSize?: number | null
|
||||
categoryId?: string | null
|
||||
storeId?: string | null
|
||||
}>(), {
|
||||
title: '',
|
||||
pageSize: 12,
|
||||
categoryId: '',
|
||||
storeId: ''
|
||||
})
|
||||
|
||||
const goodsList = ref<any[]>([])
|
||||
const params = ref({
|
||||
pageNumber: 1,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initGoods()
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始化商品
|
||||
*/
|
||||
const initGoods = async () => {
|
||||
let submit = JSON.parse(
|
||||
JSON.stringify(
|
||||
Object.assign(params.value, {
|
||||
pageSize: props.pageSize,
|
||||
categoryId: props.categoryId,
|
||||
storeId: props.storeId,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
Object.keys(submit).map((key) => {
|
||||
if (!submit[key] || (submit[key] as any).length == 0) {
|
||||
delete submit[key]
|
||||
}
|
||||
})
|
||||
let res = await getGoodsList(submit)
|
||||
goodsList.value.push(...(res.data.result.records as any[]))
|
||||
}
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -100,10 +90,6 @@ $w_94: 94%;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.goods-desc {
|
||||
border-bottom-left-radius: 20rpx;
|
||||
@@ -134,4 +120,4 @@ $w_94: 94%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<view class="serach">
|
||||
<view class="left-box" @tap="onClickLeft">
|
||||
<u-icon name="arrow-left" size="40" color="#666"></u-icon>
|
||||
<u-icon name="arrow-left" size="28rpx" color="#606266"></u-icon>
|
||||
</view>
|
||||
<view class="content" :style="{ 'border-radius': radius + 'px' }">
|
||||
<!-- HM修改 增加进入输入状态的点击范围 -->
|
||||
<view class="content-box" :class="{ center: mode === 2 }">
|
||||
<u-icon name="search" size="32" style="padding:0 15rpx;"></u-icon>
|
||||
<u-icon name="search" size="30rpx" color="#909399" style="padding:0 14rpx;"></u-icon>
|
||||
<!-- HM修改 增加placeholder input confirm-type confirm-->
|
||||
<input style="width:100%; " :placeholder="placeholder" placeholder-class="placeholder-color"
|
||||
@input="inputChange" confirm-type="search" @confirm="triggerConfirm" class="input"
|
||||
:class="{ center: !active && mode === 2 }" :focus="isFocus" v-model="inputVal" @focus="focus" @blur="blur" />
|
||||
<u-icon name="close" v-if="isDelShow" style="padding:0 30rpx;" @click="clear"></u-icon>
|
||||
<view
|
||||
class="clear-icon"
|
||||
v-if="showClear && isDelShow"
|
||||
@tap.stop.prevent="clear"
|
||||
>
|
||||
<u-icon name="close" size="28rpx" color="#909399"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -21,204 +27,214 @@
|
||||
</view>
|
||||
|
||||
<view v-else class="button-item">
|
||||
<u-icon name="grid-fill" size="50" @click="handelListClass()" v-if="!switchLayout"></u-icon>
|
||||
<u-icon v-else @click="handelListClass()" name="list-dot" size="50"></u-icon>
|
||||
<u-icon name="grid-fill" size="32rpx" color="var(--theme-light, #ff6b35)" @click="handelListClass()" v-if="!switchLayout"></u-icon>
|
||||
<u-icon v-else @click="handelListClass()" name="list-dot" size="32rpx" color="var(--theme-light, #ff6b35)"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
mode: {
|
||||
value: Number,
|
||||
default: 1,
|
||||
},
|
||||
//HM修改 定义默认搜索关键词(水印文字)
|
||||
placeholder: {
|
||||
value: String,
|
||||
default: "请输入搜索内容",
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
// 默认半径为60
|
||||
radius: {
|
||||
value: String,
|
||||
default: 60,
|
||||
},
|
||||
// 是否获取焦点
|
||||
isFocusVal: {
|
||||
value: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShowSeachGoods: false, //是否显示查询的商品
|
||||
active: false, //是否选中
|
||||
inputVal: "", //Input中内容
|
||||
isDelShow: false, //是否显示右侧删除icon
|
||||
isFocus: false, //是否获取焦点
|
||||
switchLayout: true, //切换当前商品的布局,默认为两列
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isFocus = this.isFocusVal;
|
||||
},
|
||||
methods: {
|
||||
//
|
||||
out() {
|
||||
uni.reLaunch({
|
||||
url: "/pages/tabbar/home/index",
|
||||
});
|
||||
},
|
||||
// 切换排列顺序
|
||||
handelListClass() {
|
||||
this.switchLayout = !this.switchLayout;
|
||||
this.$emit("SwitchType");
|
||||
},
|
||||
//HM修改 触发组件confirm事件
|
||||
triggerConfirm() {
|
||||
this.$emit("confirm", false);
|
||||
uni.hideKeyboard();
|
||||
},
|
||||
//HM修改 触发组件input事件
|
||||
inputChange(event) {
|
||||
var keyword = event.detail.value;
|
||||
this.$emit("input", keyword);
|
||||
if (this.inputVal) {
|
||||
this.isDelShow = true;
|
||||
}
|
||||
},
|
||||
focus() {
|
||||
this.active = true;
|
||||
//HM修改 增加获取焦点判断
|
||||
if (this.inputVal) {
|
||||
this.isDelShow = true;
|
||||
}
|
||||
},
|
||||
blur() {
|
||||
this.isFocus = false;
|
||||
if (!this.inputVal) {
|
||||
this.active = false;
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
//HM修改 收起键盘
|
||||
uni.hideKeyboard();
|
||||
this.isFocus = false;
|
||||
this.inputVal = "";
|
||||
this.active = false;
|
||||
//HM修改 清空内容时候触发组件input
|
||||
this.$emit("input", "");
|
||||
//this.$emit('search', '');//HM修改 清空内容时候不进行搜索
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
|
||||
/**
|
||||
* 回退到上一级
|
||||
*/
|
||||
onClickLeft() {
|
||||
const paths = getCurrentPages();
|
||||
console.log(paths)
|
||||
if(paths.length > 1){
|
||||
uni.navigateBack();
|
||||
}else{
|
||||
uni.switchTab({
|
||||
url:"/pages/tabbar/home/index"
|
||||
})
|
||||
}
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
mode?: number
|
||||
placeholder?: string
|
||||
modelValue?: string
|
||||
value?: string
|
||||
radius?: string
|
||||
isFocusVal?: boolean
|
||||
showClear?: boolean
|
||||
}>(), {
|
||||
mode: 1,
|
||||
placeholder: "请输入搜索内容",
|
||||
modelValue: "",
|
||||
value: "",
|
||||
radius: "60",
|
||||
isFocusVal: true,
|
||||
showClear: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* 内容为空时,输入默认关键字
|
||||
*/
|
||||
search() {
|
||||
if (!this.inputVal) {
|
||||
if (this.searchName == "取消") {
|
||||
uni.hideKeyboard();
|
||||
this.isFocus = false;
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.$emit("search", this.inputVal ? this.inputVal : this.placeholder);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* 监听当前是否有值 是否显示清除图标
|
||||
*/
|
||||
inputVal(newVal) {
|
||||
newVal ? (this.isDelShow = true) : (this.isDelShow = false);
|
||||
},
|
||||
},
|
||||
};
|
||||
const emit = defineEmits(['confirm', 'input', 'update:modelValue', 'search', 'SwitchType'])
|
||||
|
||||
const isShowSeachGoods = ref(false)
|
||||
const active = ref(false)
|
||||
const inputVal = ref("")
|
||||
const isDelShow = ref(false)
|
||||
const isFocus = ref(false)
|
||||
const switchLayout = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
isFocus.value = props.isFocusVal
|
||||
inputVal.value = props.modelValue || props.value || ''
|
||||
})
|
||||
|
||||
//
|
||||
const out = () => {
|
||||
uni.reLaunch({
|
||||
url: "/pages/tabbar/home/index",
|
||||
})
|
||||
}
|
||||
// 切换排列顺序
|
||||
const handelListClass = () => {
|
||||
switchLayout.value = !switchLayout.value
|
||||
emit("SwitchType")
|
||||
}
|
||||
//HM修改 触发组件confirm事件
|
||||
const triggerConfirm = () => {
|
||||
emit("confirm", false)
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
//HM修改 触发组件input事件
|
||||
const inputChange = (event: any) => {
|
||||
var keyword = event.detail.value
|
||||
emit("input", keyword)
|
||||
emit("update:modelValue", keyword)
|
||||
if (inputVal.value) {
|
||||
isDelShow.value = true
|
||||
}
|
||||
}
|
||||
const focus = () => {
|
||||
active.value = true
|
||||
//HM修改 增加获取焦点判断
|
||||
if (inputVal.value) {
|
||||
isDelShow.value = true
|
||||
}
|
||||
}
|
||||
const blur = () => {
|
||||
isFocus.value = false
|
||||
if (!inputVal.value) {
|
||||
active.value = false
|
||||
}
|
||||
}
|
||||
const clear = () => {
|
||||
//HM修改 收起键盘
|
||||
uni.hideKeyboard()
|
||||
isFocus.value = false
|
||||
inputVal.value = ""
|
||||
active.value = false
|
||||
isDelShow.value = false
|
||||
//HM修改 清空内容时候触发组件input
|
||||
emit("input", "")
|
||||
emit("update:modelValue", "")
|
||||
//this.$emit('search', '');//HM修改 清空内容时候不进行搜索
|
||||
}
|
||||
|
||||
const setInputValue = (value: string) => {
|
||||
inputVal.value = value || ''
|
||||
emit("input", inputVal.value)
|
||||
emit("update:modelValue", inputVal.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 回退到上一级
|
||||
*/
|
||||
const onClickLeft = () => {
|
||||
const paths = getCurrentPages()
|
||||
console.log(paths)
|
||||
if(paths.length > 1){
|
||||
uni.navigateBack()
|
||||
}else{
|
||||
uni.switchTab({
|
||||
url:"/pages/tabbar/home/index"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容为空时,输入默认关键字
|
||||
*/
|
||||
const search = () => {
|
||||
if (!inputVal.value) {
|
||||
if (false) { // searchName == "取消" - 兼容旧逻辑
|
||||
uni.hideKeyboard()
|
||||
isFocus.value = false
|
||||
active.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
emit("search", inputVal.value ? inputVal.value : props.placeholder)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听当前是否有值 是否显示清除图标
|
||||
*/
|
||||
watch(inputVal, (newVal) => {
|
||||
newVal ? (isDelShow.value = true) : (isDelShow.value = false)
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== inputVal.value) {
|
||||
inputVal.value = newVal || ''
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
isShowSeachGoods,
|
||||
inputVal,
|
||||
clear,
|
||||
setInputValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.serach {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
//border-bottom: 1px #f5f5f5 solid; //HM修改 去掉边框
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
font-size: $uni-font-size-base;
|
||||
font-size: 24rpx;
|
||||
|
||||
.left-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 15%;
|
||||
/* #ifndef APP-NVUE */
|
||||
text-align: center;
|
||||
// display: flex;
|
||||
// /* #endif */
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 70rpx;
|
||||
color: #999;
|
||||
background: #eee;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s linear;
|
||||
border-radius: 1000rpx;
|
||||
|
||||
.content-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 15rpx;
|
||||
|
||||
&.icon-serach:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
line-height: 60rpx;
|
||||
height: 60rpx;
|
||||
transition: all 0.2s linear;
|
||||
|
||||
&.center {
|
||||
width: 200rpx;
|
||||
}
|
||||
|
||||
&.sub {
|
||||
// position: absolute;
|
||||
width: auto;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 72rpx;
|
||||
height: 70rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,16 +242,16 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s linear;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
padding-left: 15rpx;
|
||||
width: 100rpx;
|
||||
padding-left: 12rpx;
|
||||
width: auto;
|
||||
min-width: 80rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 遮罩层 -->
|
||||
<u-popup @close="close" v-model="show" mode="bottom" border-radius="30" height="260rpx">
|
||||
<u-popup @close="close" v-model:show="show" mode="bottom" border-radius="30" height="260rpx">
|
||||
|
||||
<view class="share-title">
|
||||
<span>分享至</span>
|
||||
@@ -15,130 +15,138 @@
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view class="share-item" @click="handleShare(item)" v-for="(item, index) in list" :key="index">
|
||||
<u-icon :color="item.color" size="80" :name="item.icon"></u-icon>
|
||||
<view>{{ item.title }}</view>
|
||||
<u-icon class="share-icon" :color="item.color" size="80" :name="item.icon"></u-icon>
|
||||
<view class="share-label">{{ item.title }}</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="share-item" @click="copyLink()">
|
||||
<u-icon color="#b4aee8" size="80" name="share-fill"></u-icon>
|
||||
<view>{{ '复制链接' }}</view>
|
||||
<u-icon class="share-icon" color="#b4aee8" size="80" name="share-fill"></u-icon>
|
||||
<view class="share-label">{{ '复制链接' }}</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
<script>
|
||||
import { h5Copy } from "@/js_sdk/h5-copy/h5-copy.js";
|
||||
import configs from "@/config/config";
|
||||
import mpShare from "uview-ui/libs/mixin/mpShare.js";
|
||||
|
||||
export default {
|
||||
mixins: [mpShare],
|
||||
data() {
|
||||
return {
|
||||
configs,
|
||||
show: true,
|
||||
list: [
|
||||
{
|
||||
color: "#04BE02",
|
||||
title: "微信好友",
|
||||
icon: "weixin-fill",
|
||||
type: 0,
|
||||
},
|
||||
{
|
||||
color: "#04BE02",
|
||||
title: "朋友圈",
|
||||
icon: "weixin-circle-fill",
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import configs from "@/config/config"
|
||||
import mpShare from "@/utils/mpShare.js"
|
||||
import { setClipboard } from "@/utils/filters.js"
|
||||
|
||||
// mixin 处理:mpShare 提供小程序分享能力
|
||||
mpShare
|
||||
|
||||
const props = defineProps<{
|
||||
thumbnail?: string
|
||||
goodsName?: string
|
||||
type?: string
|
||||
goodsId?: string | number
|
||||
link?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const show = ref(true)
|
||||
const config = configs
|
||||
const list = ref([
|
||||
{
|
||||
color: "#04BE02",
|
||||
title: "微信好友",
|
||||
icon: "weixin-fill",
|
||||
type: 0,
|
||||
},
|
||||
// 图片缩略图、 商品名称 、 type(goods,shop,pintuan) 拼团商品分享以及店铺分享
|
||||
|
||||
props: ["thumbnail", "goodsName", "type", "goodsId", "link"],
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit("close");
|
||||
},
|
||||
weChatShare(){
|
||||
this.$u.mpShare = {
|
||||
title: this.shareTitle(), // 默认为小程序名称,可自定义
|
||||
path: '', // 默认为当前页面路径,一般无需修改,QQ小程序不支持
|
||||
// 分享图标,路径可以是本地文件路径、代码包文件路径或者网络图片路径。
|
||||
// 支持PNG及JPG,默认为当前页面的截图
|
||||
imageUrl: this.thumbnail ||''
|
||||
}
|
||||
},
|
||||
|
||||
// h5复制链接
|
||||
// #ifdef H5
|
||||
copyLink() {
|
||||
let content;
|
||||
if (this.link) {
|
||||
content = this.configs.shareLink + this.link;
|
||||
} else {
|
||||
content =
|
||||
this.configs.shareLink +
|
||||
getCurrentPages()[getCurrentPages().length - 1].__page__.fullPath;
|
||||
}
|
||||
this.$options.filters.setClipboard(content)
|
||||
},
|
||||
// #endif
|
||||
|
||||
shareTitle() {
|
||||
let shareTitle;
|
||||
if (this.type == "goods") {
|
||||
shareTitle = `[好友推荐]${this.goodsName}快来跟我一起看看吧`;
|
||||
} else if (this.type == "shops") {
|
||||
shareTitle = `[好友发现]${this.goodsName}快来跟我一起看看吧`;
|
||||
} else if (this.type == "pintuan") {
|
||||
shareTitle = `[好友邀请]${this.goodsName}快来跟我一起抢购吧!`;
|
||||
} else if (this.type == "kanjia") {
|
||||
shareTitle = `[好友邀请]请快来帮我砍一刀${this.goodsName}`;
|
||||
}
|
||||
return shareTitle;
|
||||
},
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
handleShare(val) {
|
||||
console.log("12312312")
|
||||
if (val.type <= 1) {
|
||||
let scene; // "WXSenceTimeline 朋友圈 WXSceneSession 微信好友"
|
||||
val.type == 1
|
||||
? (scene = "WXSenceTimeline")
|
||||
: (scene = "WXSceneSession");
|
||||
uni.share({
|
||||
provider: "weixin",
|
||||
scene: scene,
|
||||
href: configs.shareLink + this.link,
|
||||
imageUrl: this.thumbnail,
|
||||
type: 0,
|
||||
summary: this.goodsName,
|
||||
title: this.shareTitle(),
|
||||
success: function (res) {
|
||||
uni.showToast({
|
||||
title: "分享成功!",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
this.$emit("close");
|
||||
},
|
||||
fail: function (err) {
|
||||
uni.showToast({
|
||||
title: "分享失败!",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
this.$emit("close");
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
{
|
||||
color: "#04BE02",
|
||||
title: "朋友圈",
|
||||
icon: "weixin-circle-fill",
|
||||
type: 1,
|
||||
},
|
||||
};
|
||||
])
|
||||
|
||||
const close = () => {
|
||||
emit("close")
|
||||
}
|
||||
|
||||
const weChatShare = () => {
|
||||
// @ts-ignore
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
if (proxy) {
|
||||
// @ts-ignore
|
||||
proxy.$u.mpShare = {
|
||||
title: shareTitle(),
|
||||
path: '',
|
||||
imageUrl: props.thumbnail || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// h5复制链接
|
||||
// #ifdef H5
|
||||
const copyLink = () => {
|
||||
let content: string
|
||||
if (props.link) {
|
||||
content = config.shareLink + props.link
|
||||
} else {
|
||||
content =
|
||||
config.shareLink +
|
||||
getCurrentPages()[getCurrentPages().length - 1].__page__.fullPath
|
||||
}
|
||||
setClipboard(content)
|
||||
}
|
||||
// #endif
|
||||
|
||||
const shareTitle = (): string => {
|
||||
let shareTitle: string = ''
|
||||
if (props.type == "goods") {
|
||||
shareTitle = `[好友推荐]${props.goodsName}快来跟我一起看看吧`
|
||||
} else if (props.type == "shops") {
|
||||
shareTitle = `[好友发现]${props.goodsName}快来跟我一起看看吧`
|
||||
} else if (props.type == "pintuan") {
|
||||
shareTitle = `[好友邀请]${props.goodsName}快来跟我一起抢购吧!`
|
||||
} else if (props.type == "kanjia") {
|
||||
shareTitle = `[好友邀请]请快来帮我砍一刀${props.goodsName}`
|
||||
}
|
||||
return shareTitle
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
const handleShare = (val: any) => {
|
||||
console.log("12312312")
|
||||
if (val.type <= 1) {
|
||||
let scene: string // "WXSenceTimeline 朋友圈 WXSceneSession 微信好友"
|
||||
val.type == 1
|
||||
? (scene = "WXSenceTimeline")
|
||||
: (scene = "WXSceneSession")
|
||||
uni.share({
|
||||
provider: "weixin",
|
||||
scene: scene,
|
||||
href: config.shareLink + props.link,
|
||||
imageUrl: props.thumbnail,
|
||||
type: 0,
|
||||
summary: props.goodsName,
|
||||
title: shareTitle(),
|
||||
success: function () {
|
||||
uni.showToast({
|
||||
title: "分享成功!",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
})
|
||||
emit("close")
|
||||
},
|
||||
fail: function () {
|
||||
uni.showToast({
|
||||
title: "分享失败!",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
})
|
||||
emit("close")
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./mp-share.scss";
|
||||
@@ -155,7 +163,7 @@ export default {
|
||||
top: 30rpx;
|
||||
}
|
||||
}
|
||||
button:after {
|
||||
.share-btn:after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -168,9 +176,10 @@ button:after {
|
||||
width: 25%;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
> * {
|
||||
> .share-icon,
|
||||
> .share-label {
|
||||
margin: 8rpx 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<u-popup v-model="show" mode="bottom" height="800rpx" border-radius="14">
|
||||
<div class="wrapper">
|
||||
<view>
|
||||
<u-popup v-model:show="show" mode="bottom" height="800rpx" border-radius="14">
|
||||
<view class="wrapper">
|
||||
<view class="down-goods-tips">该商品已下架</view>
|
||||
<scroll-view scroll-y="true" style="height: 670rpx">
|
||||
<goodsRecommend title="其他商品" />
|
||||
</scroll-view>
|
||||
</div>
|
||||
</view>
|
||||
</u-popup>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import goodsRecommend from "@/components/m-goods-recommend/index.vue";
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import goodsRecommend from "@/components/m-goods-recommend/index.vue"
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: true, // 是否显示
|
||||
};
|
||||
},
|
||||
components: { goodsRecommend },
|
||||
};
|
||||
const show = ref(true)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<view class="shadow" :class="!show?'':'shadow-show'" :style="{backgroundColor:show?maskBg:'rgba(0,0,0,0)'}" @tap="tapMask">
|
||||
<view class="popups" :class="[theme]" :style="{top: popupsTop ,left: popupsLeft,flexDirection:direction}">
|
||||
<text :class="dynPlace" :style="{width:'0px',height:'0px'}" v-if="triangle"></text>
|
||||
<view v-for="(item,index) in popData" :key="index" @tap.stop="tapItem(item)" class="itemChild view" :class="[direction=='row'?'solid-right':'solid-bottom',item.disabled?'disabledColor':'']">
|
||||
<u-icon size="35" :name="item.icon" v-if="item.icon"></u-icon><span class="title">{{item.title}}</span>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
maskBg: {
|
||||
type: String,
|
||||
default: "rgba(0,0,0,0)",
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: "default", //default top-start top-end bottom-start bottom-end
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: "column", //column row
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
popData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: "light", //light dark
|
||||
},
|
||||
dynamic: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
gap: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
triangle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popupsTop: "0rpx",
|
||||
popupsLeft: "0rpx",
|
||||
show: false,
|
||||
dynPlace: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.popupsPosition();
|
||||
},
|
||||
methods: {
|
||||
tapMask() {
|
||||
this.$emit("input", !this.value);
|
||||
},
|
||||
tapItem(item) {
|
||||
if (item.disabled) return;
|
||||
this.$emit("tapPopup", item);
|
||||
this.$emit("input", !this.value);
|
||||
},
|
||||
getStatusBar() {
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
uni.getSystemInfo({
|
||||
success: function (e) {
|
||||
let customBar;
|
||||
// #ifdef H5
|
||||
|
||||
customBar = e.statusBarHeight + e.windowTop;
|
||||
|
||||
// #endif
|
||||
resolve(customBar);
|
||||
},
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
async popupsPosition() {
|
||||
let statusBar = await this.getStatusBar();
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
let popupsDom = uni.createSelectorQuery().in(this).select(".popups");
|
||||
popupsDom
|
||||
.fields(
|
||||
{
|
||||
size: true,
|
||||
},
|
||||
(data) => {
|
||||
let width = data.width;
|
||||
let height = data.height;
|
||||
|
||||
|
||||
|
||||
let y = this.dynamic
|
||||
? this.dynamicGetY(this.y, this.gap)
|
||||
: this.transformRpx(this.y);
|
||||
|
||||
let x = this.dynamic
|
||||
? this.dynamicGetX(this.x, this.gap)
|
||||
: this.transformRpx(this.x);
|
||||
|
||||
// #ifdef H5
|
||||
y = this.dynamic
|
||||
? this.y + statusBar
|
||||
: this.transformRpx(this.y + statusBar);
|
||||
// #endif
|
||||
|
||||
this.dynPlace =
|
||||
this.placement == "default"
|
||||
? this.getPlacement(x, y)
|
||||
: this.placement;
|
||||
|
||||
switch (this.dynPlace) {
|
||||
case "top-start":
|
||||
this.popupsTop = `${y + 9}rpx`;
|
||||
this.popupsLeft = `${x - 15}rpx`;
|
||||
break;
|
||||
case "top-end":
|
||||
this.popupsTop = `${y + 9}rpx`;
|
||||
this.popupsLeft = `${x + 15 - width}rpx`;
|
||||
break;
|
||||
case "bottom-start":
|
||||
this.popupsTop = `${y - 18 - height}rpx`;
|
||||
this.popupsLeft = `${x - 15}rpx`;
|
||||
break;
|
||||
case "bottom-end":
|
||||
this.popupsTop = `${y - 9 - height}rpx`;
|
||||
this.popupsLeft = `${x + 15 - width}rpx`;
|
||||
break;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
)
|
||||
.exec();
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
getPlacement(x, y) {
|
||||
let width = uni.getSystemInfoSync().windowWidth;
|
||||
let height = uni.getSystemInfoSync().windowHeight;
|
||||
if (x > width / 2 && y > height / 2) {
|
||||
return "bottom-end";
|
||||
} else if (x < width / 2 && y < height / 2) {
|
||||
return "top-start";
|
||||
} else if (x > width / 2 && y < height / 2) {
|
||||
return "top-end";
|
||||
} else if (x < width / 2 && y > height / 2) {
|
||||
return "bottom-start";
|
||||
} else if (x > width / 2) {
|
||||
return "top-end";
|
||||
} else {
|
||||
return "top-start";
|
||||
}
|
||||
},
|
||||
dynamicGetY(y, gap) {
|
||||
let height = uni.getSystemInfoSync().windowHeight;
|
||||
y = y < gap ? gap : y;
|
||||
y = height - y < gap ? height - gap : y;
|
||||
|
||||
return y;
|
||||
},
|
||||
dynamicGetX(x, gap) {
|
||||
let width = uni.getSystemInfoSync().windowWidth;
|
||||
x = x < gap ? gap : x;
|
||||
x = width - x < gap ? width - gap : x;
|
||||
return x;
|
||||
},
|
||||
transformRpx(params) {
|
||||
return (params * uni.getSystemInfoSync().screenWidth) / 375;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler: async function (newVal, oldVal) {
|
||||
if (newVal) await this.popupsPosition();
|
||||
this.show = newVal;
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
this.dynPlace = newVal;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
transition: background 0.3s ease-in-out;
|
||||
visibility: hidden;
|
||||
|
||||
&.shadow-show {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.popups {
|
||||
position: absolute;
|
||||
padding: 20rpx;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
.view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 10rpx;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
.image {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
background-color: #4c4c4c;
|
||||
color: #fff;
|
||||
.top-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #4c4c4c;
|
||||
}
|
||||
.top-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #4c4c4c;
|
||||
}
|
||||
.bottom-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #4c4c4c transparent transparent;
|
||||
}
|
||||
.bottom-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #4c4c4c transparent transparent;
|
||||
}
|
||||
.disabledColor {
|
||||
color: #c5c8ce;
|
||||
}
|
||||
}
|
||||
.light {
|
||||
color: #515a6e;
|
||||
box-shadow: 0upx 0upx 30upx rgba(0, 0, 0, 0.2);
|
||||
background: #fff;
|
||||
.top-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #fff;
|
||||
}
|
||||
.top-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #fff;
|
||||
}
|
||||
.bottom-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #fff transparent transparent;
|
||||
}
|
||||
.bottom-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #fff transparent transparent;
|
||||
}
|
||||
.disabledColor {
|
||||
color: #c5c8ce;
|
||||
}
|
||||
}
|
||||
.solid-bottom {
|
||||
border-bottom: 1px solid #f3f5f7;
|
||||
}
|
||||
.solid-right {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
.popups .itemChild:last-child {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
78
components/u-time-line/u-time-line-item.vue
Normal file
78
components/u-time-line/u-time-line-item.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<view class="u-time-line-item">
|
||||
<view class="u-time-line-item__node" :style="nodeStyle">
|
||||
<slot name="node">
|
||||
<view class="u-time-line-item__dot" :style="{ backgroundColor: bgColor }"></view>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="u-time-line-item__content">
|
||||
<slot name="content" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeTop?: string | number
|
||||
bgColor?: string
|
||||
}>()
|
||||
|
||||
const nodeStyle = computed(() => {
|
||||
if (props.nodeTop === '' || props.nodeTop === null || props.nodeTop === undefined) {
|
||||
return {}
|
||||
}
|
||||
const top = typeof props.nodeTop === 'number' ? `${props.nodeTop}rpx` : props.nodeTop
|
||||
return { top }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-time-line-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
padding-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&__node {
|
||||
position: relative;
|
||||
width: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
padding-left: 12rpx;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15rpx;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
&:last-child::before {
|
||||
bottom: auto;
|
||||
height: 16rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
components/u-time-line/u-time-line.vue
Normal file
16
components/u-time-line/u-time-line.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<view class="u-time-line">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// UTimeLine 组件
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-time-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -2,22 +2,22 @@
|
||||
<view class="uni-load-more">
|
||||
<view class="uni-load-more__img" v-show="status === 'loading' && showIcon">
|
||||
<view class="load1">
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
</view>
|
||||
<view class="load2">
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
</view>
|
||||
<view class="load3">
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
<view class="load-item" :style="{ background: color }"></view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="uni-load-more__text" :style="{ color: color }" v-if="showText">
|
||||
@@ -26,42 +26,28 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uni-load-more',
|
||||
props: {
|
||||
status: {
|
||||
//上拉的状态:more-loading前;loading-loading中;noMore-没有更多了
|
||||
type: String,
|
||||
default: 'more'
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showText: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#777777'
|
||||
},
|
||||
contentText: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
contentdown: '上拉显示更多',
|
||||
contentrefresh: '正在加载...',
|
||||
contentnomore: '没有更多数据了'
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
status?: string
|
||||
showIcon?: boolean
|
||||
showText?: boolean
|
||||
color?: string
|
||||
contentText?: {
|
||||
contentdown: string
|
||||
contentrefresh: string
|
||||
contentnomore: string
|
||||
}
|
||||
};
|
||||
}>(), {
|
||||
status: 'more',
|
||||
showIcon: true,
|
||||
showText: true,
|
||||
color: '#777777',
|
||||
contentText: () => ({
|
||||
contentdown: '上拉显示更多',
|
||||
contentrefresh: '正在加载...',
|
||||
contentnomore: '没有更多数据了',
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -86,11 +72,13 @@ export default {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view {
|
||||
.load1,
|
||||
.load2,
|
||||
.load3 {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view view {
|
||||
.load-item {
|
||||
width: 6px;
|
||||
height: 2px;
|
||||
border-top-left-radius: 1px;
|
||||
@@ -102,25 +90,25 @@ export default {
|
||||
animation: load 1.56s ease infinite;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view view:nth-child(1) {
|
||||
.load-item:nth-child(1) {
|
||||
transform: rotate(90deg);
|
||||
top: 2px;
|
||||
left: 9px;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view view:nth-child(2) {
|
||||
.load-item:nth-child(2) {
|
||||
transform: rotate(180deg);
|
||||
top: 11px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view view:nth-child(3) {
|
||||
.load-item:nth-child(3) {
|
||||
transform: rotate(270deg);
|
||||
bottom: 2px;
|
||||
left: 9px;
|
||||
}
|
||||
|
||||
.uni-load-more__img > view view:nth-child(4) {
|
||||
.load-item:nth-child(4) {
|
||||
top: 11px;
|
||||
left: 0;
|
||||
}
|
||||
@@ -140,51 +128,51 @@ export default {
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
|
||||
.load1 view:nth-child(1) {
|
||||
.load1 .load-item:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.load2 view:nth-child(1) {
|
||||
.load2 .load-item:nth-child(1) {
|
||||
animation-delay: 0.13s;
|
||||
}
|
||||
|
||||
.load3 view:nth-child(1) {
|
||||
.load3 .load-item:nth-child(1) {
|
||||
animation-delay: 0.26s;
|
||||
}
|
||||
|
||||
.load1 view:nth-child(2) {
|
||||
.load1 .load-item:nth-child(2) {
|
||||
animation-delay: 0.39s;
|
||||
}
|
||||
|
||||
.load2 view:nth-child(2) {
|
||||
.load2 .load-item:nth-child(2) {
|
||||
animation-delay: 0.52s;
|
||||
}
|
||||
|
||||
.load3 view:nth-child(2) {
|
||||
.load3 .load-item:nth-child(2) {
|
||||
animation-delay: 0.65s;
|
||||
}
|
||||
|
||||
.load1 view:nth-child(3) {
|
||||
.load1 .load-item:nth-child(3) {
|
||||
animation-delay: 0.78s;
|
||||
}
|
||||
|
||||
.load2 view:nth-child(3) {
|
||||
.load2 .load-item:nth-child(3) {
|
||||
animation-delay: 0.91s;
|
||||
}
|
||||
|
||||
.load3 view:nth-child(3) {
|
||||
.load3 .load-item:nth-child(3) {
|
||||
animation-delay: 1.04s;
|
||||
}
|
||||
|
||||
.load1 view:nth-child(4) {
|
||||
.load1 .load-item:nth-child(4) {
|
||||
animation-delay: 1.17s;
|
||||
}
|
||||
|
||||
.load2 view:nth-child(4) {
|
||||
.load2 .load-item:nth-child(4) {
|
||||
animation-delay: 1.3s;
|
||||
}
|
||||
|
||||
.load3 view:nth-child(4) {
|
||||
.load3 .load-item:nth-child(4) {
|
||||
animation-delay: 1.43s;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,211 +10,183 @@
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* NumberBox 数字输入框
|
||||
* @description 带加减按钮的数字输入框
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=31
|
||||
* @property {Number} value 输入框当前值
|
||||
* @property {Number} min 最小值
|
||||
* @property {Number} max 最大值
|
||||
* @property {Number} step 每次点击改变的间隔大小
|
||||
* @property {String} background 背景色
|
||||
* @property {String} color 字体颜色(前景色)
|
||||
* @property {Boolean} disabled = [true|false] 是否为禁用状态
|
||||
* @event {Function} change 输入框值改变时触发的事件,参数为输入框当前的 value
|
||||
* @event {Function} focus 输入框聚焦时触发的事件,参数为 event 对象
|
||||
* @event {Function} blur 输入框失焦时触发的事件,参数为 event 对象
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "UniNumberBox",
|
||||
emits: ['change', 'input', 'update:modelValue', 'blur', 'focus'],
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
modelValue: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '#f5f5f5'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#333'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.inputValue = +val;
|
||||
},
|
||||
modelValue(val) {
|
||||
this.inputValue = +val;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.value === 1) {
|
||||
this.inputValue = +this.modelValue;
|
||||
}
|
||||
if (this.modelValue === 1) {
|
||||
this.inputValue = +this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_calcValue(type) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const scale = this._getDecimalScale();
|
||||
let value = this.inputValue * scale;
|
||||
let step = this.step * scale;
|
||||
if (type === "minus") {
|
||||
value -= step;
|
||||
if (value < (this.min * scale)) {
|
||||
return;
|
||||
}
|
||||
if (value > (this.max * scale)) {
|
||||
value = this.max * scale
|
||||
}
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
if (type === "plus") {
|
||||
value += step;
|
||||
if (value > (this.max * scale)) {
|
||||
return;
|
||||
}
|
||||
if (value < (this.min * scale)) {
|
||||
value = this.min * scale
|
||||
}
|
||||
}
|
||||
const props = withDefaults(defineProps<{
|
||||
value?: number | string
|
||||
modelValue?: number | string
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
background?: string
|
||||
color?: string
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
value: 1,
|
||||
modelValue: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
background: '#f5f5f5',
|
||||
color: '#333',
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
this.inputValue = (value / scale).toFixed(String(scale).length - 1);
|
||||
this.$emit("change", +this.inputValue);
|
||||
// TODO vue2 兼容
|
||||
this.$emit("input", +this.inputValue);
|
||||
// TODO vue3 兼容
|
||||
this.$emit("update:modelValue", +this.inputValue);
|
||||
},
|
||||
_getDecimalScale() {
|
||||
const emit = defineEmits<{
|
||||
change: [value: number]
|
||||
input: [value: number]
|
||||
'update:modelValue': [value: number]
|
||||
blur: [event: Event]
|
||||
focus: [event: Event]
|
||||
}>()
|
||||
|
||||
let scale = 1;
|
||||
// 浮点型
|
||||
if (~~this.step !== this.step) {
|
||||
scale = Math.pow(10, String(this.step).split(".")[1].length);
|
||||
}
|
||||
return scale;
|
||||
},
|
||||
_onBlur(event) {
|
||||
this.$emit('blur', event)
|
||||
let value = event.detail.value;
|
||||
if (!value) {
|
||||
// this.inputValue = 0;
|
||||
return;
|
||||
}
|
||||
value = +value;
|
||||
if (value > this.max) {
|
||||
value = this.max;
|
||||
} else if (value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
const scale = this._getDecimalScale();
|
||||
this.inputValue = value.toFixed(String(scale).length - 1);
|
||||
this.$emit("change", +this.inputValue);
|
||||
this.$emit("input", +this.inputValue);
|
||||
},
|
||||
_onFocus(event) {
|
||||
this.$emit('focus', event)
|
||||
}
|
||||
const inputValue = ref(0)
|
||||
|
||||
watch(() => props.value, (val) => {
|
||||
inputValue.value = +val
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
inputValue.value = +val
|
||||
})
|
||||
|
||||
if (props.value === 1) {
|
||||
inputValue.value = +props.modelValue
|
||||
}
|
||||
if (props.modelValue === 1) {
|
||||
inputValue.value = +props.value
|
||||
}
|
||||
|
||||
function _getDecimalScale() {
|
||||
let scale = 1
|
||||
if (~~props.step !== props.step) {
|
||||
scale = Math.pow(10, String(props.step).split('.')[1].length)
|
||||
}
|
||||
return scale
|
||||
}
|
||||
|
||||
function _emitValue() {
|
||||
const val = +inputValue.value
|
||||
emit('change', val)
|
||||
emit('input', val)
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
|
||||
function _calcValue(type: 'minus' | 'plus') {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
const scale = _getDecimalScale()
|
||||
let value = inputValue.value * scale
|
||||
const step = props.step * scale
|
||||
if (type === 'minus') {
|
||||
value -= step
|
||||
if (value < props.min * scale) {
|
||||
return
|
||||
}
|
||||
};
|
||||
if (value > props.max * scale) {
|
||||
value = props.max * scale
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'plus') {
|
||||
value += step
|
||||
if (value > props.max * scale) {
|
||||
return
|
||||
}
|
||||
if (value < props.min * scale) {
|
||||
value = props.min * scale
|
||||
}
|
||||
}
|
||||
|
||||
inputValue.value = Number((value / scale).toFixed(String(scale).length - 1))
|
||||
_emitValue()
|
||||
}
|
||||
|
||||
function _onBlur(event: any) {
|
||||
emit('blur', event)
|
||||
let value = event.detail.value
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
value = +value
|
||||
if (value > props.max) {
|
||||
value = props.max
|
||||
} else if (value < props.min) {
|
||||
value = props.min
|
||||
}
|
||||
const scale = _getDecimalScale()
|
||||
inputValue.value = Number(value.toFixed(String(scale).length - 1))
|
||||
_emitValue()
|
||||
}
|
||||
|
||||
function _onFocus(event: Event) {
|
||||
emit('focus', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$box-height: 48rpx;
|
||||
$bg: #f5f5f5;
|
||||
$br: 4rpx;
|
||||
$color: #333;
|
||||
$box-height: 48rpx;
|
||||
$bg: #f5f5f5;
|
||||
$br: 4rpx;
|
||||
$color: #333;
|
||||
|
||||
.uni-numbox {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
.uni-numbox {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.uni-numbox-btns {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
background-color: $bg;
|
||||
/* #ifdef H5 */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
.uni-numbox-btns {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
background-color: $bg;
|
||||
/* #ifdef H5 */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-numbox__value {
|
||||
margin: 0 4rpx;
|
||||
background-color: $bg;
|
||||
width: 70rpx;
|
||||
height: $box-height;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
color: $color;
|
||||
}
|
||||
.uni-numbox__value {
|
||||
margin: 0 4rpx;
|
||||
background-color: $bg;
|
||||
width: 70rpx;
|
||||
height: $box-height;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.uni-numbox__minus {
|
||||
border-top-left-radius: $br;
|
||||
border-bottom-left-radius: $br;
|
||||
}
|
||||
.uni-numbox__minus {
|
||||
border-top-left-radius: $br;
|
||||
border-bottom-left-radius: $br;
|
||||
}
|
||||
|
||||
.uni-numbox__plus {
|
||||
border-top-right-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
}
|
||||
.uni-numbox__plus {
|
||||
border-top-right-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
}
|
||||
|
||||
.uni-numbox--text {
|
||||
// fix nvue
|
||||
line-height: 40rpx;
|
||||
.uni-numbox--text {
|
||||
line-height: 40rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.uni-numbox .uni-numbox--disabled {
|
||||
color: #c0c0c0 !important;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
.uni-numbox .uni-numbox--disabled {
|
||||
color: #c0c0c0 !important;
|
||||
/* #ifdef H5 */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,111 +1,99 @@
|
||||
<template></template>
|
||||
<script>
|
||||
import { getAddressCode } from "@/api/address";
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { getAddressCode } from '@/api/address'
|
||||
import config from '@/config/config'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
// 初始化地图并且调用
|
||||
initMap() {
|
||||
let that = this;
|
||||
uni.chooseLocation({
|
||||
success: function (res) {
|
||||
/**获取地址详情地址 */
|
||||
that.posToCity(res.latitude, res.longitude).then((val) => {
|
||||
/**获取地址code */
|
||||
getAddressCode(
|
||||
val.regeocode.addressComponent.citycode,
|
||||
val.regeocode.addressComponent.township
|
||||
).then((code) => {
|
||||
that.$emit("callback", { ...val, ...res, ...code });
|
||||
that.$emit("close");
|
||||
});
|
||||
});
|
||||
},
|
||||
fail(e) {
|
||||
console.log(e)
|
||||
that.$emit("close");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 根据当前客户端判断展示不同类型数据
|
||||
init() {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.wechatMap();
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
this.initMap();
|
||||
// #endif
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
callback: [payload: Record<string, any>]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
// 如果是微信小程序单独走微信小程序授权模式
|
||||
wechatMap() {
|
||||
let that = this;
|
||||
uni.authorize({
|
||||
scope: "scope.userLocation",
|
||||
success() {
|
||||
// 允许授权
|
||||
that.initMap();
|
||||
},
|
||||
fail() {
|
||||
//拒绝授权
|
||||
uni.showModal({
|
||||
content: "检测到您没打开获取地址功能权限,是否去设置打开?",
|
||||
confirmText: "确认",
|
||||
cancelText: "取消",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 打开设置好后重新刷新地图
|
||||
uni.openSetting({
|
||||
success: (res) => {
|
||||
that.initMap();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 取消后关闭
|
||||
that.$emit("close");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
return false;
|
||||
},
|
||||
});
|
||||
},
|
||||
// 获取城市的数据
|
||||
posToCity(latitude, longitude) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `https://restapi.amap.com/v3/geocode/regeo`,
|
||||
method: "GET",
|
||||
data: {
|
||||
key: config.aMapKey, //web服务的key
|
||||
location: `${longitude},${latitude}`,
|
||||
},
|
||||
success: ({ data }) => {
|
||||
const { status, info } = data;
|
||||
if (status === "1") {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(info);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
function initMap() {
|
||||
uni.chooseLocation({
|
||||
success(res) {
|
||||
posToCity(res.latitude, res.longitude).then((val) => {
|
||||
getAddressCode(
|
||||
val.regeocode.addressComponent.citycode,
|
||||
val.regeocode.addressComponent.township,
|
||||
).then((code) => {
|
||||
emit('callback', { ...val, ...res, ...code })
|
||||
emit('close')
|
||||
})
|
||||
})
|
||||
},
|
||||
fail(e) {
|
||||
console.log(e)
|
||||
emit('close')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function init() {
|
||||
// #ifdef MP-WEIXIN
|
||||
wechatMap()
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
initMap()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function wechatMap() {
|
||||
uni.authorize({
|
||||
scope: 'scope.userLocation',
|
||||
success() {
|
||||
initMap()
|
||||
},
|
||||
fail() {
|
||||
uni.showModal({
|
||||
content: '检测到您没打开获取地址功能权限,是否去设置打开?',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.openSetting({
|
||||
success: () => {
|
||||
initMap()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function posToCity(latitude: number, longitude: number) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
uni.request({
|
||||
url: 'https://restapi.amap.com/v3/geocode/regeo',
|
||||
method: 'GET',
|
||||
data: {
|
||||
key: config.aMapKey,
|
||||
location: `${longitude},${latitude}`,
|
||||
},
|
||||
success: ({ data }: any) => {
|
||||
const { status, info } = data
|
||||
if (status === '1') {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(info)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,284 +1,356 @@
|
||||
<template>
|
||||
<view>
|
||||
<view
|
||||
v-if="!hid"
|
||||
v-if="!state.hid"
|
||||
class="flex-row-center"
|
||||
:style="{ top: scHight }"
|
||||
style="width: 750rpx; position: fixed; z-index: 100; left: 0"
|
||||
:style="{ top: state.scHight }"
|
||||
style="width: 750rpx; position: fixed; z-index: 100; left: 0; background-color: #ffffff; padding: 40rpx 0"
|
||||
>
|
||||
<view
|
||||
class="flex-column-center"
|
||||
style="background-color: #fcfcfc; padding: 30rpx; border-radius: 10rpx"
|
||||
>
|
||||
<view class="flex-column-center">
|
||||
<movable-area
|
||||
class="flex"
|
||||
style="width: 100%"
|
||||
animation="false"
|
||||
:style="{ height: originalHeight }"
|
||||
:style="{ height: state.originalHeight }"
|
||||
>
|
||||
<movable-view
|
||||
scale-value="1"
|
||||
animation="false"
|
||||
damping="5000"
|
||||
:x="moveX"
|
||||
:x="state.moveX"
|
||||
:style="{
|
||||
height: sliderHeight,
|
||||
width: sliderWidth,
|
||||
height: state.sliderHeight,
|
||||
width: state.sliderWidth,
|
||||
'z-index': 101,
|
||||
}"
|
||||
direction="horizontal"
|
||||
>
|
||||
<image
|
||||
:src="imgbk"
|
||||
:src="state.imgbk"
|
||||
class="image"
|
||||
mode="aspectFit"
|
||||
:style="{
|
||||
height: sliderHeight,
|
||||
width: sliderWidth,
|
||||
'margin-top': imgbKH,
|
||||
height: state.sliderHeight,
|
||||
width: state.sliderWidth,
|
||||
'margin-top': state.imgbKH,
|
||||
}"
|
||||
></image>
|
||||
</movable-view>
|
||||
<image
|
||||
:src="img"
|
||||
:src="state.img"
|
||||
mode="aspectFit"
|
||||
:style="{ height: originalHeight, width: originalWidth }"
|
||||
style="border-radius: 10rpx"
|
||||
:style="{ height: state.originalHeight, width: state.originalWidth }"
|
||||
style="border-radius: 24rpx"
|
||||
></image>
|
||||
</movable-area>
|
||||
|
||||
<movable-area
|
||||
class="flex-row-start"
|
||||
style="
|
||||
width: 100%;
|
||||
background-color: #efefef;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-top: 30rpx;
|
||||
"
|
||||
>
|
||||
<movable-area class="v-track" animation="false">
|
||||
<view class="v-fill" :style="{ width: fillWidth }"></view>
|
||||
<text class="v-track-text" :style="{ color: state.col }">{{
|
||||
state.hasImg
|
||||
}}</text>
|
||||
<movable-view
|
||||
scale-value="1"
|
||||
animation="false"
|
||||
damping="50"
|
||||
:x="movePv"
|
||||
class="flex-row-center"
|
||||
style="
|
||||
border-radius: 50%;
|
||||
height: 100rpx;
|
||||
width: 100rpx;
|
||||
background-color: #ffffff;
|
||||
border: 2rpx solid #e3e3e3;
|
||||
margin-top: -13rpx;
|
||||
"
|
||||
:x="state.movePv"
|
||||
class="v-handle"
|
||||
:style="handleStyle"
|
||||
direction="horizontal"
|
||||
@change="moveChange"
|
||||
@touchend="end"
|
||||
>
|
||||
<u-icon
|
||||
:color="mainColor"
|
||||
size="40"
|
||||
v-if="endLoad"
|
||||
color="#ffffff"
|
||||
size="38"
|
||||
v-if="state.endLoad"
|
||||
name="arrow-right"
|
||||
></u-icon>
|
||||
<u-icon :color="mainColor" size="40" v-else name="reload"></u-icon>
|
||||
<u-icon color="#ffffff" size="38" v-else name="reload"></u-icon>
|
||||
</movable-view>
|
||||
|
||||
<text style="padding-left: 140rpx" :style="{ color: col }">{{
|
||||
hasImg
|
||||
}}</text>
|
||||
</movable-area>
|
||||
<view class="flex-row-around padding-top" style="width: 100%">
|
||||
<u-icon
|
||||
@click="hide"
|
||||
:color="mainColor"
|
||||
size="40"
|
||||
name="close"
|
||||
></u-icon>
|
||||
|
||||
<text class="cu-tag round" @click="getCode">刷新拼图</text>
|
||||
<text class="my-neirong-sm cuIcon-safe" style="color: #c1c1c1"
|
||||
>Lili-FRAMEWORK</text
|
||||
>
|
||||
<view class="v-foot">
|
||||
<view class="v-icon-btn" @click="getCode">
|
||||
<u-icon color="#888888" size="32" name="reload"></u-icon>
|
||||
</view>
|
||||
<view class="v-icon-btn" @click="hide">
|
||||
<u-icon color="#888888" size="30" name="close"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="v-mark">
|
||||
<text class="cuIcon-safe v-mark-icon"></text>
|
||||
<text>Lili-FRAMEWORK 安全验证</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/config/api.js";
|
||||
import storage from "@/utils/storage.js";
|
||||
import uuid from "@/utils/uuid.modified.js";
|
||||
const phone = uni.getSystemInfoSync();
|
||||
const l = phone.screenWidth / 750;
|
||||
export default {
|
||||
name: "verification",
|
||||
created() {
|
||||
// 可自行调整
|
||||
this.scHight = phone.screenHeight / 2 - 200 + "px";
|
||||
this.getCode();
|
||||
},
|
||||
props: {
|
||||
height: {
|
||||
type: String,
|
||||
default: "80rpx",
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import api from '@/config/api.js'
|
||||
import storage from '@/utils/storage.js'
|
||||
import uuid from '@/utils/uuid.modified.js'
|
||||
|
||||
const emit = defineEmits(['send'])
|
||||
const props = withDefaults(defineProps<{
|
||||
height?: string
|
||||
width?: string
|
||||
left?: string
|
||||
top?: string
|
||||
business?: string
|
||||
apiType?: string
|
||||
}>(), {
|
||||
height: '80rpx',
|
||||
width: '350rpx',
|
||||
left: '180rpx',
|
||||
top: '30rpx',
|
||||
business: 'LOGIN',
|
||||
apiType: 'buyer'
|
||||
})
|
||||
|
||||
const sliderBaseUrl = computed(() => {
|
||||
return props.apiType === 'store' ? api.store : api.buyer
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
const phone = uni.getSystemInfoSync()
|
||||
const l = phone.screenWidth / 750
|
||||
|
||||
const mainColor = computed(() => store.getters.mainColor)
|
||||
const fillWidth = computed(() => `${state.moveX + 42 * l}px`)
|
||||
const handleStyle = computed(() => {
|
||||
const glow = /^#[0-9a-fA-F]{6}$/.test(mainColor.value)
|
||||
? mainColor.value + '59'
|
||||
: 'rgba(0, 0, 0, 0.18)'
|
||||
return { background: mainColor.value, boxShadow: `0 6rpx 20rpx ${glow}` }
|
||||
})
|
||||
const state = reactive({
|
||||
flage: false,
|
||||
key: '', // key
|
||||
vsrtx: '点击进行验证', // 按钮提示语
|
||||
vsr: false,
|
||||
hid: true,
|
||||
col: '#838383',
|
||||
movePv: 0,
|
||||
hasImg: '拖动滑块已完成拼图',
|
||||
spcode: '',
|
||||
tl: 0,
|
||||
moveCode: 0,
|
||||
// X轴移动距离
|
||||
moveX: 0,
|
||||
// 模版高度
|
||||
originalHeight: '',
|
||||
// 模版宽度
|
||||
originalWidth: '',
|
||||
// 拼图高度
|
||||
sliderHeight: '',
|
||||
// 平涂宽度
|
||||
sliderWidth: '',
|
||||
scHight: 0,
|
||||
// 原图
|
||||
img: '',
|
||||
// 拼图
|
||||
imgbk: '',
|
||||
endLoad: true,
|
||||
imgbKH: ''
|
||||
})
|
||||
|
||||
// 可自行调整
|
||||
state.scHight = phone.screenHeight / 2 - 200 + 'px'
|
||||
getCode()
|
||||
|
||||
function show() {
|
||||
state.hid = false
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!state.vsr) {
|
||||
// vsr判断是否验证成功,成功隐藏验证框
|
||||
state.hid = !state.hid
|
||||
}
|
||||
}
|
||||
|
||||
function error() {
|
||||
state.vsr = false
|
||||
state.hid = false
|
||||
state.moveX = 0
|
||||
state.moveCode = 0
|
||||
}
|
||||
|
||||
// 获取验证图片
|
||||
function getCode() {
|
||||
state.col = '#b3afae'
|
||||
state.hasImg = '图片加载中...'
|
||||
if (!storage.getUuid()) {
|
||||
storage.setUuid(uuid.v1())
|
||||
}
|
||||
uni.request({
|
||||
url: sliderBaseUrl.value + '/common/slider/' + props.business,
|
||||
header: {
|
||||
uuid: storage.getUuid()
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "350rpx",
|
||||
success: (res: any) => {
|
||||
state.col = '#838383'
|
||||
state.hasImg = '拖动滑块以完成拼图'
|
||||
const data = res.data.result
|
||||
|
||||
// base64的图片
|
||||
state.img = data.backImage
|
||||
state.imgbk = data.slidingImage
|
||||
// 根据参数动态适应验证图片的高宽
|
||||
state.imgbKH = data.randomY * 1.8 + 'rpx'
|
||||
state.originalHeight = data.originalHeight * 1.8 + 'rpx'
|
||||
state.originalWidth = data.originalWidth * 1.8 + 'rpx'
|
||||
state.sliderHeight = data.sliderHeight * 1.8 + 'rpx'
|
||||
state.sliderWidth = data.sliderWidth * 1.8 + 'rpx'
|
||||
// 适应比率,用来适应滑动距离
|
||||
state.tl = 1 / (1.8 * l)
|
||||
// 无用信息
|
||||
state.spcode = data.capcode
|
||||
// 验证令牌
|
||||
state.key = data.key
|
||||
store.state.verificationKey = data.key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function end() {
|
||||
state.endLoad = false
|
||||
// 验证拼图位置是否正确
|
||||
uni.request({
|
||||
method: 'POST',
|
||||
url:
|
||||
sliderBaseUrl.value +
|
||||
'/common/slider/' +
|
||||
props.business +
|
||||
'?xPos=' +
|
||||
parseInt(String(state.moveCode * state.tl)),
|
||||
header: {
|
||||
uuid: storage.getUuid()
|
||||
},
|
||||
left: {
|
||||
type: String,
|
||||
default: "180rpx",
|
||||
},
|
||||
top: {
|
||||
type: String,
|
||||
default: "30rpx",
|
||||
},
|
||||
business: {
|
||||
type: String,
|
||||
default: "LOGIN",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mainColor: this.$mainColor,
|
||||
flage: false,
|
||||
key: "", //key
|
||||
vsrtx: "点击进行验证", //按钮提示语
|
||||
vsr: false, //
|
||||
hid: true,
|
||||
col: "#838383",
|
||||
movePv: 0,
|
||||
hasImg: "拖动滑块已完成拼图",
|
||||
spcode: "",
|
||||
tl: 0,
|
||||
moveCode: 0,
|
||||
//X轴移动距离
|
||||
moveX: 0,
|
||||
//模版高度
|
||||
originalHeight: "",
|
||||
//模版宽度
|
||||
originalWidth: "",
|
||||
//拼图高度
|
||||
sliderHeight: "",
|
||||
//平涂宽度
|
||||
sliderWidth: "",
|
||||
scHight: 0,
|
||||
//原图
|
||||
img: "",
|
||||
//拼图
|
||||
imgbk: "",
|
||||
endLoad: true,
|
||||
imgbKH: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.hid = false;
|
||||
},
|
||||
hide() {
|
||||
if (!this.vsr) {
|
||||
// vsr判断是否验证成功,成功隐藏验证框
|
||||
this.hid = !this.hid;
|
||||
success: (res: any) => {
|
||||
state.endLoad = true
|
||||
if (res.data.result == false) {
|
||||
res.data.result = false
|
||||
} else {
|
||||
res.data.result = true
|
||||
}
|
||||
|
||||
if (res.data && res.data.result) {
|
||||
// 验证成功后把key发送出去,后端会把验证信息存在缓存里
|
||||
emit('send', state.key)
|
||||
hide()
|
||||
state.vsr = true
|
||||
state.vsrtx = '已通过验证'
|
||||
} else {
|
||||
getCode() // 让滑块回到起始位置
|
||||
if (state.movePv == 1) {
|
||||
state.movePv = 0
|
||||
} else {
|
||||
state.movePv = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
error() {
|
||||
this.vsr = false;
|
||||
this.hid = false;
|
||||
this.moveX = 0;
|
||||
this.moveCode = 0;
|
||||
},
|
||||
// 获取验证图片
|
||||
getCode() {
|
||||
this.col = "#b3afae";
|
||||
this.hasImg = "图片加载中...";
|
||||
if (!storage.getUuid()) {
|
||||
storage.setUuid(uuid.v1());
|
||||
}
|
||||
uni.request({
|
||||
url: api.common + "/common/slider/" + this.business,
|
||||
header: {
|
||||
uuid: storage.getUuid(),
|
||||
},
|
||||
success: (res) => {
|
||||
this.col = "#838383";
|
||||
this.hasImg = "拖动滑块以完成拼图";
|
||||
var data = res.data.result;
|
||||
fail: () => {
|
||||
uni.showToast({ title: '连接服务器失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// base64的图片
|
||||
this.img = data.backImage;
|
||||
this.imgbk = data.slidingImage;
|
||||
// 根据参数动态适应验证图片的高宽
|
||||
this.imgbKH = data.randomY * 1.8 + "rpx";
|
||||
this.originalHeight = data.originalHeight * 1.8 + "rpx";
|
||||
this.originalWidth = data.originalWidth * 1.8 + "rpx";
|
||||
this.sliderHeight = data.sliderHeight * 1.8 + "rpx";
|
||||
this.sliderWidth = data.sliderWidth * 1.8 + "rpx";
|
||||
// 适应比率,用来适应滑动距离
|
||||
this.tl = 1 / (1.8 * l);
|
||||
// 无用信息
|
||||
this.spcode = data.capcode;
|
||||
// 验证令牌
|
||||
this.key = data.key;
|
||||
this.$store.state.verificationKey = data.key;
|
||||
},
|
||||
});
|
||||
},
|
||||
end(e) {
|
||||
this.endLoad = false;
|
||||
// 验证拼图位置是否正确
|
||||
uni.request({
|
||||
method: "POST",
|
||||
url:
|
||||
api.common +
|
||||
"/common/slider/" +
|
||||
this.business +
|
||||
"?xPos=" +
|
||||
parseInt(this.moveCode * this.tl),
|
||||
header: {
|
||||
uuid: storage.getUuid(),
|
||||
},
|
||||
success: (res) => {
|
||||
this.endLoad = true;
|
||||
res.data.result == false
|
||||
? (res.data.result = false)
|
||||
: (res.data.result = true);
|
||||
// 绑定拼图位置
|
||||
function moveChange(e: any) {
|
||||
state.moveX = e.detail.x
|
||||
state.moveCode = e.detail.x
|
||||
}
|
||||
|
||||
if (res.data && res.data.result) {
|
||||
//验证成功后把key发送出去,后端会把验证信息存在缓存里
|
||||
this.$emit("send", this.key);
|
||||
this.hide();
|
||||
this.vsr = true;
|
||||
this.vsrtx = "已通过验证";
|
||||
} else {
|
||||
this.getCode(); // 让滑块回到起始位置
|
||||
if (this.movePv == 1) {
|
||||
this.movePv = 0;
|
||||
} else {
|
||||
this.movePv = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (res) => {
|
||||
this.$msg("连接服务器失败");
|
||||
},
|
||||
});
|
||||
},
|
||||
// 绑定拼图位置
|
||||
moveChange(e) {
|
||||
this.moveX = e.detail.x;
|
||||
this.moveCode = e.detail.x;
|
||||
},
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
error,
|
||||
getCode,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./animation.css";
|
||||
@import "./icon.css";
|
||||
// @import './main.css';
|
||||
|
||||
.v-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
margin-top: 36rpx;
|
||||
background-color: #f7f7f8;
|
||||
border-radius: 42rpx;
|
||||
}
|
||||
|
||||
.v-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffe9e5;
|
||||
border-radius: 42rpx;
|
||||
}
|
||||
|
||||
.v-track-text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
line-height: 84rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.v-handle {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.v-foot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.v-icon-btn {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f4f4f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.v-mark {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
font-size: 22rpx;
|
||||
color: #d2d2d2;
|
||||
}
|
||||
|
||||
.v-mark-icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
.dh-wt {
|
||||
animation: at 1.1s ease;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<view class="xt__verify-code">
|
||||
<!-- 输入框 -->
|
||||
<input
|
||||
ref="codeInput"
|
||||
id="xt__input"
|
||||
:value="code"
|
||||
class="xt__input"
|
||||
:focus="isFocus"
|
||||
:focus="focused"
|
||||
:password="isPassword"
|
||||
:type="inputType"
|
||||
:maxlength="size"
|
||||
@input="input"
|
||||
@input="handleInput"
|
||||
@focus="inputFocus"
|
||||
@blur="inputBlur"
|
||||
/>
|
||||
@@ -23,21 +24,20 @@
|
||||
></view>
|
||||
|
||||
<!-- 输入框 - 组 -->
|
||||
<view id="xt__input-ground" class="xt__input-ground">
|
||||
<template v-for="(item, index) in size">
|
||||
<view id="xt__input-ground" class="xt__input-ground" @click="focusInput">
|
||||
<template v-for="(item, index) in size" :key="index">
|
||||
<view
|
||||
:key="index"
|
||||
:style="{ borderColor: code.length === index && cursorVisible ? boxActiveColor : boxNormalColor }"
|
||||
:class="['xt__box', `xt__box-${type + ''}`, `xt__box::after`]"
|
||||
>
|
||||
<view :style="{ borderColor: boxActiveColor }" class="xt__middle-line" v-if="type === 'middle' && !code[index]"></view>
|
||||
<text class="xt__code-text">{{ code[index] | codeFormat(isPassword) }}</text>
|
||||
<text class="xt__code-text">{{ codeFormat(code[index], isPassword) }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 输入验证码组件
|
||||
* @property {string} type = [box|middle|bottom] - 显示类型 默认:box -eg:bottom
|
||||
@@ -50,151 +50,156 @@
|
||||
* @property {string} boxActiveColor - 光标聚焦到的框的颜色 默认:#000000
|
||||
* @event {Function(data)} confirm - 输入完成
|
||||
*/
|
||||
export default {
|
||||
name: 'xt-verify-code',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: () => 'box'
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: () => 'number'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: () => 6
|
||||
},
|
||||
isFocus: {
|
||||
type: Boolean,
|
||||
default: () => true
|
||||
},
|
||||
isPassword: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
cursorColor: {
|
||||
type: String,
|
||||
default: () => '#cccccc'
|
||||
},
|
||||
boxNormalColor: {
|
||||
type: String,
|
||||
default: () => '#cccccc'
|
||||
},
|
||||
boxActiveColor: {
|
||||
type: String,
|
||||
default: () => '#000000'
|
||||
}
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cursorVisible: false,
|
||||
cursorHeight: 35,
|
||||
code: '', // 输入的验证码
|
||||
codeCursorLeft: [] // 向左移动的距离数组
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.cursorVisible = this.isFocus;
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @description 初始化
|
||||
*/
|
||||
init() {
|
||||
this.getCodeCursorLeft();
|
||||
this.setCursorHeight();
|
||||
},
|
||||
/**
|
||||
* @description 获取元素节点
|
||||
* @param {string} elm - 节点的id、class 相当于 document.querySelect的参数 -eg: #id
|
||||
* @param {string} type = [single|array] - 单个元素获取多个元素 默认是单个元素
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
getElement(elm, type = 'single', callback) {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
[type === 'array' ? 'selectAll' : 'select'](elm)
|
||||
.boundingClientRect()
|
||||
.exec(data => {
|
||||
callback(data[0]);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @description 计算光标的高度
|
||||
*/
|
||||
setCursorHeight() {
|
||||
this.getElement('.xt__box', 'single', boxElm => {
|
||||
this.cursorHeight = boxElm.height * 0.6;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @description 获取光标在每一个box的left位置
|
||||
*/
|
||||
getCodeCursorLeft() {
|
||||
// 获取父级框的位置信息
|
||||
this.getElement('#xt__input-ground', 'single', parentElm => {
|
||||
const parentLeft = parentElm.left;
|
||||
// 获取各个box信息
|
||||
this.getElement('.xt__box', 'array', elms => {
|
||||
this.codeCursorLeft = [];
|
||||
elms.forEach(elm => {
|
||||
this.codeCursorLeft.push(elm.left - parentLeft + elm.width / 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
import { ref, watch, onMounted, nextTick, getCurrentInstance } from 'vue'
|
||||
|
||||
// 输入框输入变化的回调
|
||||
input(e) {
|
||||
const value = e.detail.value;
|
||||
this.cursorVisible = value.length !== this.size;
|
||||
this.$emit('input', value);
|
||||
this.inputSuccess(value);
|
||||
},
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'confirm'])
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
value?: string
|
||||
type?: string
|
||||
inputType?: string
|
||||
size?: number
|
||||
isFocus?: boolean
|
||||
isPassword?: boolean
|
||||
cursorColor?: string
|
||||
boxNormalColor?: string
|
||||
boxActiveColor?: string
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
value: '',
|
||||
type: 'box',
|
||||
inputType: 'number',
|
||||
size: 6,
|
||||
isFocus: true,
|
||||
isPassword: false,
|
||||
cursorColor: '#cccccc',
|
||||
boxNormalColor: '#cccccc',
|
||||
boxActiveColor: '#000000'
|
||||
})
|
||||
|
||||
// 输入完成回调
|
||||
inputSuccess(value) {
|
||||
if (value.length === this.size) {
|
||||
this.$emit('confirm', value);
|
||||
}
|
||||
},
|
||||
// 输入聚焦
|
||||
inputFocus() {
|
||||
this.cursorVisible = this.code.length !== this.size;
|
||||
},
|
||||
// 输入失去焦点
|
||||
inputBlur() {
|
||||
this.cursorVisible = false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.code = val;
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
codeFormat(val, isPassword) {
|
||||
let value = '';
|
||||
if (val) {
|
||||
value = isPassword ? '*' : val;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
const instance = getCurrentInstance()
|
||||
const codeInput = ref<any>(null)
|
||||
const focused = ref(false)
|
||||
const cursorVisible = ref(false)
|
||||
const cursorHeight = ref(35)
|
||||
const code = ref('') // 输入的验证码
|
||||
const codeCursorLeft = ref<number[]>([]) // 向左移动的距离数组
|
||||
|
||||
// 初始化
|
||||
watch([() => props.modelValue, () => props.value], ([modelVal, val]) => {
|
||||
code.value = modelVal || val || ''
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
focused.value = props.isFocus
|
||||
cursorVisible.value = props.isFocus
|
||||
init()
|
||||
if (props.isFocus) {
|
||||
nextTick(() => {
|
||||
focusInput()
|
||||
})
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
function focusInput() {
|
||||
focused.value = false
|
||||
nextTick(() => {
|
||||
focused.value = true
|
||||
// #ifdef H5
|
||||
const inputEl = codeInput.value
|
||||
if (inputEl && typeof inputEl.focus === 'function') {
|
||||
inputEl.focus()
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 初始化
|
||||
*/
|
||||
function init() {
|
||||
getCodeCursorLeft()
|
||||
setCursorHeight()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取元素节点
|
||||
* @param {string} elm - 节点的id、class 相当于 document.querySelect的参数 -eg: #id
|
||||
* @param {string} type = [single|array] - 单个元素获取多个元素 默认是单个元素
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
function getElement(elm: string, type: string = 'single', callback: Function) {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(instance?.proxy)
|
||||
[type === 'array' ? 'selectAll' : 'select'](elm)
|
||||
.boundingClientRect()
|
||||
.exec((data: any) => {
|
||||
callback(data[0])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 计算光标的高度
|
||||
*/
|
||||
function setCursorHeight() {
|
||||
getElement('.xt__box', 'single', (boxElm: any) => {
|
||||
cursorHeight.value = boxElm.height * 0.6
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取光标在每一个box的left位置
|
||||
*/
|
||||
function getCodeCursorLeft() {
|
||||
// 获取父级框的位置信息
|
||||
getElement('#xt__input-ground', 'single', (parentElm: any) => {
|
||||
const parentLeft = parentElm.left
|
||||
// 获取各个box信息
|
||||
getElement('.xt__box', 'array', (elms: any) => {
|
||||
codeCursorLeft.value = []
|
||||
elms.forEach((elm: any) => {
|
||||
codeCursorLeft.value.push(elm.left - parentLeft + elm.width / 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 输入框输入变化的回调
|
||||
function handleInput(e: any) {
|
||||
const inputValue = e?.detail?.value ?? e?.target?.value ?? ''
|
||||
const value = String(inputValue).replace(/\D/g, '').slice(0, props.size)
|
||||
code.value = value
|
||||
cursorVisible.value = value.length !== props.size
|
||||
emit('update:modelValue', value)
|
||||
emit('input', value)
|
||||
inputSuccess(value)
|
||||
}
|
||||
|
||||
// 输入完成回调
|
||||
function inputSuccess(value: string) {
|
||||
if (value.length === props.size) {
|
||||
emit('confirm', value)
|
||||
}
|
||||
}
|
||||
|
||||
// 输入聚焦
|
||||
function inputFocus() {
|
||||
cursorVisible.value = code.value.length !== props.size
|
||||
}
|
||||
|
||||
// 输入失去焦点
|
||||
function inputBlur() {
|
||||
cursorVisible.value = false
|
||||
}
|
||||
|
||||
function codeFormat(val: string | undefined, isPassword: boolean): string {
|
||||
let value = ''
|
||||
if (val) {
|
||||
value = isPassword ? '*' : val
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.xt__verify-code {
|
||||
@@ -203,11 +208,13 @@ export default {
|
||||
box-sizing: border-box;
|
||||
|
||||
.xt__input {
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.xt__cursor {
|
||||
position: absolute;
|
||||
@@ -215,6 +222,8 @@ export default {
|
||||
transform: translateY(-50%);
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
animation-name: cursor;
|
||||
animation-duration: 0.8s;
|
||||
animation-iteration-count: infinite;
|
||||
@@ -226,11 +235,14 @@ export default {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.xt__box {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 76rpx;
|
||||
height: 112rpx;
|
||||
pointer-events: none;
|
||||
&-bottom {
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
@@ -259,13 +271,40 @@ export default {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-size:52rpx;
|
||||
z-index: 1;
|
||||
color: #333;
|
||||
font-size: 52rpx;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
/*
|
||||
* App 的原生 input 如果覆盖在验证码格上,部分 Android 设备会出现
|
||||
* 原生光标/输入绘制层与自绘数字冲突。沿用 u-code-input 的隐藏输入方式,
|
||||
* 将输入内容和原生光标移出可视区域,验证码格只负责展示响应式状态。
|
||||
*/
|
||||
.xt__verify-code {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xt__input {
|
||||
left: -750rpx;
|
||||
width: 1500rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xt__input-ground {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.xt__cursor {
|
||||
z-index: 3;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
@keyframes cursor {
|
||||
0% {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
/**
|
||||
* base : 基础业务API
|
||||
* buyer : 买家API
|
||||
*/
|
||||
// 开发环境
|
||||
const dev = {
|
||||
im: "https://im-api.pickmall.cn",
|
||||
common: "https://common-api.pickmall.cn",
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
// common: "http://192.168.0.113:8890",
|
||||
// buyer: "http://192.168.0.113:8888",
|
||||
// im: "http://192.168.0.113:8885",
|
||||
};
|
||||
// 生产环境
|
||||
const prod = {
|
||||
im: "https://im-api.pickmall.cn",
|
||||
common: "https://common-api.pickmall.cn",
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
};
|
||||
|
||||
//默认生产环境
|
||||
let api = dev;
|
||||
//如果是开发环境
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
api = dev;
|
||||
} else {
|
||||
api = prod;
|
||||
}
|
||||
//微信小程序,app的打包方式建议为生产环境,所以这块直接条件编译赋值
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
api = prod;
|
||||
// #endif
|
||||
|
||||
api.buyer += "/buyer";
|
||||
api.common += "/common";
|
||||
api.im += "/im";
|
||||
export default {
|
||||
...api,
|
||||
};
|
||||
/**
|
||||
* buyer : 买家 API(baseURL 为 buyer + /buyer)
|
||||
* store : 商家 API(baseURL 为 store + /store)
|
||||
*/
|
||||
// 开发环境
|
||||
const dev = {
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
store: "https://admin-api.pickmall.cn",
|
||||
im: "https://im-api.pickmall.cn",
|
||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||
};
|
||||
// 生产环境
|
||||
const prod = {
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
store: "https://admin-api.pickmall.cn",
|
||||
im: "https://im-api.pickmall.cn",
|
||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||
};
|
||||
|
||||
let api = dev;
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
api = dev;
|
||||
} else {
|
||||
api = prod;
|
||||
}
|
||||
// 微信小程序、App 打包使用生产环境配置
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
api = prod;
|
||||
// #endif
|
||||
|
||||
const buyerHost = api.buyer;
|
||||
const storeHost = api.store;
|
||||
api.buyer = buyerHost + "/buyer";
|
||||
api.store = storeHost + "/store";
|
||||
api.im = api.im + "/im";
|
||||
|
||||
export default {
|
||||
...api,
|
||||
buyerHost,
|
||||
storeHost,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
imWebSrc: "https://im.pickmall.cn", //IM地址
|
||||
baseWsUrl: "wss://im-api.pickmall.cn/lili/webSocket", // IM WS 地址
|
||||
enableGetClipboard: false, //是否启用粘贴板获取 scanAuthNavigation 中的链接,如果匹配则会跳转到对应页面
|
||||
enableMiniBarStartUpApp: true, //是否在h5中右侧浮空按钮点击启动app
|
||||
enableMiniBarStartUpApp: false, //是否在h5中右侧浮空按钮点击启动app
|
||||
/**
|
||||
* 如需更换主题请修改此处以及uni.scss中的全局颜色
|
||||
*/
|
||||
|
||||
23
index.html
Normal file
23
index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<title></title>
|
||||
<!-- H5 构建会把 process.env 替换成空对象,但运行时仍会读取 process,浏览器中不存在该变量 -->
|
||||
<script>
|
||||
if (typeof process === "undefined") {
|
||||
window.process = { env: {} };
|
||||
}
|
||||
</script>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,18 +2,105 @@
|
||||
* 微信小程序支付
|
||||
* 此处针对于微信小程序开发的支付插件
|
||||
* 第一次支付成功后会跳出订阅的消息 如果用户拒绝或同意都会跳转到支付成功页面
|
||||
* 如果点击订阅 会将状态写进缓存 之后不再提醒。
|
||||
*
|
||||
* @param {sn,price}
|
||||
*
|
||||
* @param {sn,price,orderPromotionType}
|
||||
*/
|
||||
|
||||
import { getWeChatMpMessage } from "@/api/message.js";
|
||||
import { initiatePay } from "@/api/trade";
|
||||
|
||||
const SCENE_NAME_MAP = {
|
||||
ORDER_DELIVER: ["订单发货提醒", "订单发货通知"],
|
||||
ORDER_COMPLETE: ["订单已完成提醒", "订单完成通知"],
|
||||
PINTUAN_SUCCESS: ["拼团成功通知"],
|
||||
AFTER_SALE_AUDIT: ["售后申请审核通知", "售后审核通知"],
|
||||
POINT_CHANGE: ["积分变更提醒", "积分变更通知"],
|
||||
WALLET_CHANGE: ["账户余额变动通知", "余额账户变更通知"],
|
||||
WALLET_WITHDRAWAL_AUDIT_ERROR: ["提现结果通知", "提现申请审核通知"],
|
||||
};
|
||||
|
||||
let cachedMpMessageList = null;
|
||||
|
||||
export function prefetchMpMessageTemplates() {
|
||||
return getWeChatMpMessage()
|
||||
.then((res) => {
|
||||
if (res && res.data && Array.isArray(res.data.result)) {
|
||||
cachedMpMessageList = res.data.result;
|
||||
}
|
||||
return cachedMpMessageList || [];
|
||||
})
|
||||
.catch(() => cachedMpMessageList || []);
|
||||
}
|
||||
|
||||
function extractTmplIdsByScenes(list, targetScenes) {
|
||||
const tmplIds = [];
|
||||
(targetScenes || []).forEach((scene) => {
|
||||
const aliasNames = SCENE_NAME_MAP[scene] || [];
|
||||
const matched = (list || []).find(
|
||||
(item) =>
|
||||
item &&
|
||||
item.enable !== false &&
|
||||
item.code &&
|
||||
(item.sceneCode === scene || aliasNames.includes(item.name))
|
||||
);
|
||||
if (matched && matched.code && !tmplIds.includes(matched.code)) {
|
||||
tmplIds.push(matched.code);
|
||||
}
|
||||
});
|
||||
return tmplIds.slice(0, 3);
|
||||
}
|
||||
|
||||
export function subscribeMpMessageByScenes(sceneCodes, onComplete) {
|
||||
const done = typeof onComplete === "function" ? onComplete : () => {};
|
||||
const targetScenes = Array.isArray(sceneCodes) ? sceneCodes : [];
|
||||
if (!targetScenes.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(cachedMpMessageList) && cachedMpMessageList.length) {
|
||||
const finalTmplIds = extractTmplIdsByScenes(cachedMpMessageList, targetScenes);
|
||||
if (!finalTmplIds.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
console.log("[微信订阅消息] 请求订阅 tmplIds:", finalTmplIds);
|
||||
uni.requestSubscribeMessage({
|
||||
tmplIds: finalTmplIds,
|
||||
complete: (res) => {
|
||||
console.log("[微信订阅消息] 授权返回结果:", res);
|
||||
done();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
prefetchMpMessageTemplates()
|
||||
.then((list) => {
|
||||
const finalTmplIds = extractTmplIdsByScenes(list, targetScenes);
|
||||
if (!finalTmplIds.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
uni.requestSubscribeMessage({
|
||||
tmplIds: finalTmplIds,
|
||||
complete: () => done(),
|
||||
});
|
||||
})
|
||||
.catch(() => done());
|
||||
}
|
||||
|
||||
export function subscribePayMessageThen(onComplete, options = {}) {
|
||||
const isPintuan =
|
||||
options &&
|
||||
String(options.orderPromotionType || "").toUpperCase() === "PINTUAN";
|
||||
const scenes = isPintuan
|
||||
? ["ORDER_DELIVER", "ORDER_COMPLETE", "PINTUAN_SUCCESS"]
|
||||
: ["ORDER_DELIVER", "ORDER_COMPLETE"];
|
||||
subscribeMpMessageByScenes(scenes, onComplete);
|
||||
}
|
||||
|
||||
class LiLiWXPay {
|
||||
constructor(...payList) {
|
||||
this.data = payList[0];
|
||||
console.log(payList);
|
||||
// 调用支付
|
||||
this.pay = () => {
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
@@ -26,7 +113,6 @@ class LiLiWXPay {
|
||||
};
|
||||
const paymentMethod = "WECHAT";
|
||||
const paymentClient = "MP";
|
||||
// 调用支付
|
||||
initiatePay(paymentMethod, paymentClient, submitData).then((res) => {
|
||||
let response = res.data.result;
|
||||
uni.hideLoading();
|
||||
@@ -38,16 +124,25 @@ class LiLiWXPay {
|
||||
package: response.package,
|
||||
signType: response.signType,
|
||||
paySign: response.paySign,
|
||||
success: (e) => {
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
sendMessage(payList[0].price);
|
||||
subscribePayMessageThen(
|
||||
() => {
|
||||
uni.redirectTo({
|
||||
url:
|
||||
"/pages/cart/payment/success?paymentMethod=WECHAT" +
|
||||
"&payPrice=" +
|
||||
payList[0].price,
|
||||
});
|
||||
},
|
||||
{ orderPromotionType: this.data.orderPromotionType }
|
||||
);
|
||||
},
|
||||
fail: (e) => {
|
||||
this.exception = e;
|
||||
// 支付异常或支付失败之后跳转到订单页面
|
||||
uni.showModal({
|
||||
content: "支付失败,如果您已支付,请勿反复支付",
|
||||
showCancel: false,
|
||||
@@ -64,41 +159,4 @@ class LiLiWXPay {
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(price) {
|
||||
|
||||
|
||||
//订阅消息
|
||||
getWeChatMpMessage().then((res) => {
|
||||
var message = res.data.result;
|
||||
var templateid = message.map((item) => item.code);
|
||||
uni.requestSubscribeMessage({
|
||||
tmplIds: templateid,
|
||||
success: (res) => {
|
||||
|
||||
},
|
||||
fail: (res) => {
|
||||
console.log('fail', res)
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "订阅消息失败",
|
||||
})
|
||||
},
|
||||
complete: (res) => {
|
||||
console.log('complete', res)
|
||||
|
||||
/**
|
||||
* 已经支付成功
|
||||
*/
|
||||
uni.redirectTo({
|
||||
url:
|
||||
"/pages/cart/payment/success?paymentMethod=WECHAT" +
|
||||
"&payPrice=" +
|
||||
price,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export default LiLiWXPay;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
var b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
|
||||
exports.weBtoa = function (string) {
|
||||
export function weBtoa(string) {
|
||||
string = String(string);
|
||||
var bitmap, a, b, c, result = "", i = 0, rest = string.length % 3;
|
||||
for (; i < string.length;) {
|
||||
@@ -15,8 +14,8 @@ exports.weBtoa = function (string) {
|
||||
b64.charAt(bitmap >> 6 & 63) + b64.charAt(bitmap & 63);
|
||||
}
|
||||
return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
|
||||
};
|
||||
exports.weAtob = function (string) {
|
||||
}
|
||||
export function weAtob(string) {
|
||||
string = String(string).replace(/[\t\n\f\r ]+/g, "");
|
||||
if (!b64re.test(string))
|
||||
throw new TypeError("Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.");
|
||||
@@ -30,9 +29,9 @@ exports.weAtob = function (string) {
|
||||
String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255, bitmap & 255);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(exports.weAtob(str).replace(/(.)/g, function (p) {
|
||||
return decodeURIComponent(weAtob(str).replace(/(.)/g, function (p) {
|
||||
var code = p.charCodeAt(0).toString(16).toUpperCase();
|
||||
if (code.length < 2) {
|
||||
code = "0" + code;
|
||||
@@ -58,7 +57,7 @@ function base64_url_decode(str) {
|
||||
return b64DecodeUnicode(output);
|
||||
}
|
||||
catch (err) {
|
||||
return exports.weAtob(output);
|
||||
return weAtob(output);
|
||||
}
|
||||
}
|
||||
function weappJwtDecode(token, options) {
|
||||
@@ -74,4 +73,4 @@ function weappJwtDecode(token, options) {
|
||||
throw ("Invalid token specified: " + e.message);
|
||||
}
|
||||
}
|
||||
exports.default = weappJwtDecode;
|
||||
export default weappJwtDecode;
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
var _a;
|
||||
export const PLATFORM = typeof process !== 'undefined' ? (_a = process === null || process === void 0 ? void 0 : process.env) === null || _a === void 0 ? void 0 : _a.VUE_APP_PLATFORM : undefined;
|
||||
function detectPlatform() {
|
||||
if (typeof process !== 'undefined' && (_a = process === null || process === void 0 ? void 0 : process.env) !== null && _a !== void 0 && _a.VUE_APP_PLATFORM) {
|
||||
return process.env.VUE_APP_PLATFORM;
|
||||
}
|
||||
if (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function') {
|
||||
return 'mp-weixin';
|
||||
}
|
||||
if (typeof uni !== 'undefined' && typeof uni.getSystemInfoSync === 'function') {
|
||||
const info = uni.getSystemInfoSync();
|
||||
if (info && info.uniPlatform === 'mp-weixin') {
|
||||
return 'mp-weixin';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const PLATFORM = detectPlatform();
|
||||
/** 全局对象 */
|
||||
const _uni = (function () {
|
||||
if (typeof uni != "undefined")
|
||||
|
||||
@@ -37,7 +37,9 @@ export const handleBuildOpts = (options) => {
|
||||
}
|
||||
const oldSelector = defaultOpts.selector;
|
||||
if (PLATFORM === 'mp-weixin' && defaultOpts.type2d) {
|
||||
defaultOpts.selector = '#' + defaultOpts.selector;
|
||||
defaultOpts.selector = oldSelector.startsWith('#')
|
||||
? oldSelector
|
||||
: '#' + oldSelector;
|
||||
}
|
||||
if (!PLATFORM) {
|
||||
console.error('注意! draw-poster未开启uni条件编译! 当环境是微信小程序将不会动态切换为type2d模式');
|
||||
|
||||
@@ -265,7 +265,7 @@ function checkSystemEnableLocation() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
judgeIosPermission: judgeIosPermission,
|
||||
requestAndroidPermission: requestAndroidPermission,
|
||||
checkSystemEnableLocation: checkSystemEnableLocation,
|
||||
|
||||
146
main.js
146
main.js
@@ -1,59 +1,87 @@
|
||||
import Vue from "vue";
|
||||
import App from "./App";
|
||||
import * as filters from "./utils/filters.js"; // global filter
|
||||
import uView from "uview-ui";
|
||||
import store from "./store";
|
||||
import config from '@/config/config';
|
||||
import airBtn from "@/components/m-airbtn/index.vue";
|
||||
import socketIO from './pages/mine/im/socket';
|
||||
/**
|
||||
* 仅在h5中显示唤醒app功能
|
||||
* 在h5页面手动挂载
|
||||
*
|
||||
*/
|
||||
// #ifdef H5
|
||||
if (config.enableMiniBarStartUpApp) {
|
||||
let btn = Vue.component("airBtn", airBtn); //全局注册
|
||||
document.body.appendChild(new btn().$mount().$el);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 引入uView对小程序分享的mixin封装
|
||||
let mpShare = require('uview-ui/libs/mixin/mpShare.js');
|
||||
Vue.mixin(mpShare)
|
||||
|
||||
/**
|
||||
* 全局filters
|
||||
*/
|
||||
|
||||
Object.keys(filters).forEach((key) => {
|
||||
Vue.filter(key, filters[key]);
|
||||
});
|
||||
|
||||
// 引入Vuex
|
||||
Vue.prototype.$store = store;
|
||||
// Vue.prototype.socketIo = new socketIO();
|
||||
Vue.use(uView);
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
/**
|
||||
* 注意!
|
||||
* 此处将常用的颜色嵌入到原型链上面
|
||||
* 颜色使用驼峰命名对应 uni.scss中全局颜色变量名
|
||||
* 如需更换主题请修改此处以及uni.scss中的全局颜色
|
||||
*/
|
||||
// 主题色
|
||||
Vue.prototype.$mainColor = config.mainColor;
|
||||
// 高亮主题色
|
||||
Vue.prototype.$lightColor = config.lightColor;
|
||||
// 辅助高亮颜色
|
||||
Vue.prototype.$aiderLightColor = config.aiderLightColor;
|
||||
|
||||
|
||||
App.mpType = "app";
|
||||
|
||||
const app = new Vue({
|
||||
...App,
|
||||
});
|
||||
app.$mount();
|
||||
import { createSSRApp, computed } from 'vue'
|
||||
import App from './App'
|
||||
import uviewPlus, { setConfig } from 'uview-plus'
|
||||
import store from './store'
|
||||
import config from '@/config/config'
|
||||
// #ifdef H5
|
||||
import airBtn from '@/components/m-airbtn/index.vue'
|
||||
// #endif
|
||||
import mpShare from '@/utils/mpShare.js'
|
||||
import * as filterUtils from './utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
// uview-plus 内部会触发 Vue3 开发态告警(枚举组件实例 keys),不影响运行
|
||||
app.config.warnHandler = (msg, _instance, trace) => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.includes('enumerating keys on a component instance')
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.warn(`[Vue warn]: ${msg}${trace || ''}`)
|
||||
} catch (e) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信开发者工具部分版本在输出 Vue warn 时会触发
|
||||
// TypeError: Cannot read property '__global' of null
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
app.use(store)
|
||||
app.use(uviewPlus)
|
||||
|
||||
// uview-plus 的图标字体默认每个 u-icon 实例都会触发 uni.loadFontFace,
|
||||
// 导致进入页面时同一字体被反复请求。开启 loadFontOnce 后全局只加载一次。
|
||||
setConfig({
|
||||
config: {
|
||||
loadFontOnce: true,
|
||||
iconUrl: 'https://lilishop-uniapp.oss-cn-beijing.aliyuncs.com/font_2225171_8kdcwk4po24.ttf',
|
||||
},
|
||||
props: {
|
||||
form: {
|
||||
labelWidth: '180rpx',
|
||||
labelPosition: 'left',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
app.config.globalProperties.$store = store
|
||||
app.config.globalProperties.$mainColor = computed(() => store.getters.mainColor)
|
||||
app.config.globalProperties.$lightColor = computed(() => store.getters.lightColor)
|
||||
app.config.globalProperties.$aiderLightColor = computed(
|
||||
() => store.getters.aiderLightColor
|
||||
)
|
||||
|
||||
const filterMethods = {}
|
||||
Object.keys(filterUtils).forEach((key) => {
|
||||
if (typeof filterUtils[key] === 'function') {
|
||||
filterMethods[key] = filterUtils[key]
|
||||
}
|
||||
})
|
||||
|
||||
app.mixin({
|
||||
methods: filterMethods,
|
||||
computed: {
|
||||
themeStyle() {
|
||||
return getThemeStyle(this.$store.state.theme)
|
||||
},
|
||||
},
|
||||
})
|
||||
app.mixin(mpShare)
|
||||
|
||||
// #ifdef H5
|
||||
if (config.enableMiniBarStartUpApp) {
|
||||
const mountPoint = document.createElement('div')
|
||||
document.body.appendChild(mountPoint)
|
||||
createSSRApp(airBtn).mount(mountPoint)
|
||||
}
|
||||
// #endif
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"vueVersion" : "3",
|
||||
"name" : "lilishop",
|
||||
"appid" : "__UNI__ED807EA",
|
||||
"description" : "",
|
||||
"versionName" : "4.5.3",
|
||||
"versionCode" : 4000503,
|
||||
"versionName" : "5.03",
|
||||
"versionCode" : 503,
|
||||
"transformPx" : false,
|
||||
"app-plus" : {
|
||||
"compatible" : {
|
||||
@@ -191,6 +192,7 @@
|
||||
"mp-weixin" : {
|
||||
/* 小程序特有相关 */
|
||||
"usingComponents" : true,
|
||||
"lazyCodeLoading" : "requiredComponents",
|
||||
"appid" : "wx6f10f29075dc1b0b",
|
||||
"optimization" : {
|
||||
"subPackages" : true
|
||||
@@ -198,8 +200,9 @@
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"minified" : true,
|
||||
"postcss" : false,
|
||||
"es6" : true
|
||||
"postcss" : true,
|
||||
"es6" : true,
|
||||
"enhance" : true
|
||||
},
|
||||
"permission" : {
|
||||
"scope.userLocation" : {
|
||||
@@ -207,12 +210,9 @@
|
||||
}
|
||||
},
|
||||
"plugins" : {},
|
||||
// 直播插件注释
|
||||
// "live-player-plugin" : {
|
||||
// "version" : "1.3.0",
|
||||
// "provider" : "wx2b03c6e691cd7370"
|
||||
// }
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
||||
"libVersion" : "3.5.8",
|
||||
"__usePrivacyCheck__" : false,
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation", "chooseAddress" ]
|
||||
},
|
||||
"h5" : {
|
||||
"devServer" : {
|
||||
|
||||
3027
package-lock.json
generated
Normal file
3027
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "lilishop-uniapp",
|
||||
"version": "4.5.3",
|
||||
"description": "Lilishop uni-app mobile client (Vue 3)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && node scripts/patch-mqtt-abort.js",
|
||||
"test:mqtt-app-runtime": "node scripts/test-mqtt-app-runtime.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.5.5",
|
||||
"hls.js": "^1.6.16",
|
||||
"mqtt": "5.15.1",
|
||||
"sass": "^1.89.2",
|
||||
"uview-plus": "3.8.42",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"patch-package": "^8.0.1",
|
||||
"postinstall-postinstall": "^2.1.0"
|
||||
}
|
||||
}
|
||||
26
pages-sub/merchant/api/evaluation.js
Normal file
26
pages-sub/merchant/api/evaluation.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { storeHttp } from "../utils/storeRequest.js";
|
||||
|
||||
/**
|
||||
* 分页获取客户评价列表
|
||||
* @param {Object} params { grade, goodsName, replyStatus, pageNumber, pageSize }
|
||||
*/
|
||||
export function getStoreEvaluationList(params) {
|
||||
return storeHttp.request({
|
||||
url: "/member/evaluation",
|
||||
method: "GET",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复客户评价
|
||||
* @param {String} id 评价ID
|
||||
* @param {String} reply 回复文字
|
||||
* @param {String} replyImage 可选回复图片
|
||||
*/
|
||||
export function replyStoreEvaluation(id, reply, replyImage = "") {
|
||||
return storeHttp.request({
|
||||
url: `/member/evaluation/reply/${id}?reply=${encodeURIComponent(reply)}&replyImage=${encodeURIComponent(replyImage)}`,
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
70
pages-sub/merchant/api/goods.js
Normal file
70
pages-sub/merchant/api/goods.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { storeHttp } from "../utils/storeRequest.js";
|
||||
|
||||
/**
|
||||
* 获取商家商品列表
|
||||
* @param {Object} params { goodsName, marketEnable, authFlag, pageNumber, pageSize }
|
||||
*/
|
||||
export function getStoreGoodsList(params) {
|
||||
return storeHttp.request({
|
||||
url: "/goods/goods/list",
|
||||
method: "GET",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商家商品详情
|
||||
* @param {String} id 商品ID
|
||||
*/
|
||||
export function getStoreGoodsDetail(id) {
|
||||
return storeHttp.request({
|
||||
url: `/goods/goods/get/${id}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品关联的 SKU 列表
|
||||
* @param {String} goodsId 商品ID
|
||||
*/
|
||||
export function getStoreGoodsSkuList(goodsId) {
|
||||
return storeHttp.request({
|
||||
url: `/goods/goods/sku/${goodsId}/list`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上架商品
|
||||
* @param {String|Array} goodsId
|
||||
*/
|
||||
export function upGoods(goodsId) {
|
||||
const ids = Array.isArray(goodsId) ? goodsId.join(",") : goodsId;
|
||||
return storeHttp.request({
|
||||
url: `/goods/goods/up?goodsId=${ids}`,
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下架商品
|
||||
* @param {String|Array} goodsId
|
||||
*/
|
||||
export function downGoods(goodsId) {
|
||||
const ids = Array.isArray(goodsId) ? goodsId.join(",") : goodsId;
|
||||
return storeHttp.request({
|
||||
url: `/goods/goods/under?goodsId=${ids}`,
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品统计数量(出售中、已下架等)
|
||||
*/
|
||||
export function getStoreGoodsNumber(params) {
|
||||
return storeHttp.request({
|
||||
url: "/goods/goods/goodsNumber",
|
||||
method: "GET",
|
||||
params,
|
||||
});
|
||||
}
|
||||
104
pages-sub/merchant/api/order.js
Normal file
104
pages-sub/merchant/api/order.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { storeHttp } from "../utils/storeRequest.js";
|
||||
|
||||
/**
|
||||
* 商家订单列表
|
||||
* @param {Object} params { orderStatus, orderType, keywords, pageNumber, pageSize }
|
||||
*/
|
||||
export function getStoreOrderList(params) {
|
||||
return storeHttp.request({
|
||||
url: "/order/order",
|
||||
method: "GET",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 商家订单详情
|
||||
* @param {String} sn
|
||||
*/
|
||||
export function getStoreOrderDetail(sn) {
|
||||
return storeHttp.request({
|
||||
url: `/order/order/${sn}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自提 / 虚拟商品单码核销
|
||||
* @param {String} sn
|
||||
* @param {String} verificationCode
|
||||
*/
|
||||
export function storeOrderTake(sn, verificationCode) {
|
||||
return storeHttp.request({
|
||||
url: `/order/order/take/${sn}/${verificationCode}`,
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用核销预检(按核销码查询)
|
||||
* @param {String} code
|
||||
*/
|
||||
export function previewStoreVerification(code) {
|
||||
return storeHttp.request({
|
||||
url: "/order/order/verification/preview",
|
||||
method: "GET",
|
||||
params: { code },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用确认核销
|
||||
* @param {String} code
|
||||
*/
|
||||
export function confirmStoreVerification(code) {
|
||||
return storeHttp.request({
|
||||
url: "/order/order/verification/confirm",
|
||||
method: "POST",
|
||||
data: { code },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约服务 — 根据核销码查询预约订单详情
|
||||
* @param {String} code
|
||||
*/
|
||||
export function getAppointmentOrderByCode(code) {
|
||||
return storeHttp.request({
|
||||
url: `/appointment/verification/order/${code}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约服务 — 执行核销
|
||||
* @param {Object} params { orderSn, code, serviceStaffId }
|
||||
*/
|
||||
export function verifyAppointmentCode(params) {
|
||||
return storeHttp.request({
|
||||
url: "/appointment/verification/verify",
|
||||
method: "POST",
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约服务 — 获取已启用的服务人员列表(供核销时指派/确认)
|
||||
*/
|
||||
export function getEnabledServiceStaff() {
|
||||
return storeHttp.request({
|
||||
url: "/appointment/service-staff/enabled",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预约服务 — 根据订单号获取预约单核销状态
|
||||
* @param {String} orderSn
|
||||
*/
|
||||
export function getAppointmentVerificationByOrderSn(orderSn) {
|
||||
return storeHttp.request({
|
||||
url: `/appointment/verification/order-by-sn/${orderSn}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
94
pages-sub/merchant/api/passport.js
Normal file
94
pages-sub/merchant/api/passport.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { storeHttp } from "../utils/storeRequest.js";
|
||||
|
||||
/**
|
||||
* 账号密码登录(支持店主与店员账号)
|
||||
* @param {Object} params { username, password }
|
||||
*/
|
||||
export function userLogin(params) {
|
||||
return storeHttp.request({
|
||||
url: "/passport/login/userLogin",
|
||||
method: "POST",
|
||||
params,
|
||||
header: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录(支持店主与店员手机号)
|
||||
* @param {Object} params { mobile, code }
|
||||
*/
|
||||
export function smsLogin(params) {
|
||||
return storeHttp.request({
|
||||
url: "/passport/login/smsLogin",
|
||||
method: "POST",
|
||||
params,
|
||||
header: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录短信
|
||||
* @param {String} mobile
|
||||
* @param {String} verificationEnums (LOGIN)
|
||||
*/
|
||||
export function sendStoreSms(mobile, verificationEnums = "LOGIN") {
|
||||
return storeHttp.request({
|
||||
url: `/common/sms/${verificationEnums}/${mobile}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取滑块验证码
|
||||
* @param {String} verificationEnums (LOGIN)
|
||||
*/
|
||||
export function getStoreSlider(verificationEnums = "LOGIN") {
|
||||
return storeHttp.request({
|
||||
url: `/common/slider/${verificationEnums}`,
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
export function storeLogout() {
|
||||
return storeHttp.request({
|
||||
url: "/passport/login/logout",
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店铺基本配置与信息
|
||||
*/
|
||||
export function getStoreSettings() {
|
||||
return storeHttp.request({
|
||||
url: "/settings/storeSettings",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录人员(店主 / 店员)个人信息
|
||||
*/
|
||||
export function getStoreUserInfo() {
|
||||
return storeHttp.request({
|
||||
url: "/member/user/info",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录所属店铺详情
|
||||
*/
|
||||
export function getStoreDetail() {
|
||||
return storeHttp.request({
|
||||
url: "/member/user",
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
650
pages-sub/merchant/pages/evaluation/list.vue
Normal file
650
pages-sub/merchant/pages/evaluation/list.vue
Normal file
@@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<view class="merchant-evaluation-page">
|
||||
<!-- 顶部 Tab 过滤 -->
|
||||
<view class="sticky-header">
|
||||
<view class="tab-scroll-wrap">
|
||||
<view
|
||||
class="tab-item"
|
||||
v-for="item in tabList"
|
||||
:key="item.key"
|
||||
:class="{ active: currentTab === item.key }"
|
||||
@click="switchTab(item.key)"
|
||||
>
|
||||
<text class="tab-label">{{ item.label }}</text>
|
||||
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评价列表主体 -->
|
||||
<view class="eval-list-body">
|
||||
<view
|
||||
class="eval-card"
|
||||
v-for="item in evalList"
|
||||
:key="item.id"
|
||||
>
|
||||
<!-- 头部:买家信息与星级 -->
|
||||
<view class="eval-header">
|
||||
<view class="user-box">
|
||||
<image
|
||||
class="user-avatar"
|
||||
:src="item.memberProfile || '/static/tabbar/home.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="user-meta">
|
||||
<text class="user-name">{{ maskName(item.memberName) }}</text>
|
||||
<text class="eval-time">{{ item.createTime || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 评分 -->
|
||||
<view class="rating-stars">
|
||||
<u-icon
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
name="star-fill"
|
||||
size="14"
|
||||
:color="star <= (item.descriptionScore || 5) ? '#f59e0b' : '#e5e7eb'"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评价文字内容 -->
|
||||
<view class="eval-content">
|
||||
<text class="content-text">{{ item.content || '用户未填写文字评价' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 买家晒图 -->
|
||||
<view class="eval-images" v-if="getImages(item.images).length > 0">
|
||||
<image
|
||||
class="eval-pic"
|
||||
v-for="(img, idx) in getImages(item.images)"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
@click="previewImages(getImages(item.images), idx)"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 关联商品简要信息 -->
|
||||
<view class="goods-snippet" v-if="item.goodsName">
|
||||
<image
|
||||
class="goods-snippet-img"
|
||||
:src="item.goodsImage || '/static/tabbar/home.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="goods-snippet-title">{{ item.goodsName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 商家已回复内容 -->
|
||||
<view class="seller-reply-box" v-if="item.reply">
|
||||
<view class="reply-title-row">
|
||||
<text class="reply-tag">掌柜回复:</text>
|
||||
<text class="reply-time" v-if="item.replyTime">{{ item.replyTime }}</text>
|
||||
</view>
|
||||
<text class="reply-text">{{ item.reply }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 回复动作条 -->
|
||||
<view class="eval-footer">
|
||||
<view
|
||||
class="reply-trigger-btn"
|
||||
@click="openReplyModal(item)"
|
||||
>
|
||||
<u-icon name="chat" size="14" :color="item.reply ? '#6b7280' : '#ffffff'"></u-icon>
|
||||
<text :class="{ 'is-replied': item.reply }">
|
||||
{{ item.reply ? '修改回复' : '立即回复' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && evalList.length === 0">
|
||||
<u-icon name="chat" size="56" color="#d1d5db"></u-icon>
|
||||
<text class="empty-text">暂无相关评价数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="loading-more" v-if="loading">
|
||||
<text>正在加载评价数据...</text>
|
||||
</view>
|
||||
<view class="no-more" v-else-if="finished && evalList.length > 0">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回复弹窗 -->
|
||||
<view class="reply-modal-mask" v-if="showReplyModal" @click.self="closeReplyModal">
|
||||
<view class="reply-modal-card">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">回复买家评价</text>
|
||||
<view class="close-btn" @click="closeReplyModal">
|
||||
<u-icon name="close" size="18" color="#9ca3af"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<textarea
|
||||
class="reply-textarea"
|
||||
v-model="replyText"
|
||||
placeholder="请输入真诚的掌柜回复内容,让买家感受到热情与专业..."
|
||||
placeholder-class="placeholder"
|
||||
maxlength="300"
|
||||
></textarea>
|
||||
<view class="char-count">
|
||||
<text>{{ replyText.length }}/300</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeReplyModal">取消</view>
|
||||
<view
|
||||
class="modal-btn confirm"
|
||||
:class="{ loading: submittingReply }"
|
||||
@click="submitReply"
|
||||
>
|
||||
{{ submittingReply ? '提交中...' : '提交回复' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getStoreEvaluationList, replyStoreEvaluation } from '../../api/evaluation.js'
|
||||
|
||||
const currentTab = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const evalList = ref([])
|
||||
|
||||
const showReplyModal = ref(false)
|
||||
const currentEval = ref(null)
|
||||
const replyText = ref('')
|
||||
const submittingReply = ref(false)
|
||||
|
||||
const tabList = [
|
||||
{ key: 'ALL', label: '全部' },
|
||||
{ key: 'GOOD', label: '好评' },
|
||||
{ key: 'POOR', label: '中差评' },
|
||||
{ key: 'UNREPLIED', label: '待回复' },
|
||||
]
|
||||
|
||||
onLoad(() => {
|
||||
fetchEvaluations(true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchEvaluations(true)
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!finished.value && !loading.value) {
|
||||
pageNumber.value++
|
||||
fetchEvaluations(false)
|
||||
}
|
||||
})
|
||||
|
||||
const fetchEvaluations = async (reset = false) => {
|
||||
if (reset) {
|
||||
pageNumber.value = 1
|
||||
evalList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const params = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
|
||||
if (currentTab.value === 'GOOD') {
|
||||
params.grade = 'GOOD'
|
||||
} else if (currentTab.value === 'POOR') {
|
||||
params.grade = 'POOR'
|
||||
} else if (currentTab.value === 'UNREPLIED') {
|
||||
params.replyStatus = false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getStoreEvaluationList(params)
|
||||
loading.value = false
|
||||
if (res.data && res.data.success && res.data.result) {
|
||||
const records = res.data.result.records || []
|
||||
if (reset) {
|
||||
evalList.value = records
|
||||
} else {
|
||||
evalList.value = [...evalList.value, ...records]
|
||||
}
|
||||
if (records.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
} else {
|
||||
finished.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (tabKey) => {
|
||||
if (currentTab.value === tabKey) return
|
||||
currentTab.value = tabKey
|
||||
fetchEvaluations(true)
|
||||
}
|
||||
|
||||
const maskName = (name) => {
|
||||
if (!name) return '匿名买家'
|
||||
if (name.length <= 2) return name[0] + '*'
|
||||
return name[0] + '***' + name[name.length - 1]
|
||||
}
|
||||
|
||||
const getImages = (imgs) => {
|
||||
if (!imgs) return []
|
||||
if (Array.isArray(imgs)) return imgs
|
||||
if (typeof imgs === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(imgs)
|
||||
if (Array.isArray(parsed)) return parsed
|
||||
} catch (e) {
|
||||
return imgs.split(',').filter(Boolean)
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const previewImages = (urls, current) => {
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current,
|
||||
})
|
||||
}
|
||||
|
||||
// 打开回复弹窗
|
||||
const openReplyModal = (item) => {
|
||||
currentEval.value = item
|
||||
replyText.value = item.reply || ''
|
||||
showReplyModal.value = true
|
||||
}
|
||||
|
||||
const closeReplyModal = () => {
|
||||
showReplyModal.value = false
|
||||
currentEval.value = null
|
||||
replyText.value = ''
|
||||
}
|
||||
|
||||
// 提交回复
|
||||
const submitReply = async () => {
|
||||
if (!replyText.value.trim()) {
|
||||
uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (submittingReply.value) return
|
||||
|
||||
submittingReply.value = true
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
|
||||
try {
|
||||
const res = await replyStoreEvaluation(currentEval.value.id, replyText.value.trim())
|
||||
uni.hideLoading()
|
||||
submittingReply.value = false
|
||||
|
||||
if (res.data && res.data.success) {
|
||||
uni.showToast({ title: '回复成功', icon: 'success' })
|
||||
closeReplyModal()
|
||||
fetchEvaluations(true)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data?.message || '回复失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
submittingReply.value = false
|
||||
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-evaluation-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f3f4f6;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.tab-scroll-wrap {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding: 0 16rpx;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
position: relative;
|
||||
|
||||
.tab-label {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-label {
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
background: #111827;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.eval-list-body {
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
.eval-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
.eval-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
margin-left: 16rpx;
|
||||
|
||||
.user-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.eval-time {
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 4rpx;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
display: flex;
|
||||
gap: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.eval-content {
|
||||
margin-top: 20rpx;
|
||||
|
||||
.content-text {
|
||||
font-size: 28rpx;
|
||||
color: #1f2937;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.eval-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
|
||||
.eval-pic {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-snippet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
border-radius: 12rpx;
|
||||
padding: 14rpx;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.goods-snippet-img {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-snippet-title {
|
||||
font-size: 22rpx;
|
||||
color: #4b5563;
|
||||
margin-left: 14rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.seller-reply-box {
|
||||
margin-top: 20rpx;
|
||||
background: #f3f4f6;
|
||||
border-left: 6rpx solid #111827;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
|
||||
.reply-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.reply-tag {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-text {
|
||||
font-size: 26rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.eval-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f9fafb;
|
||||
|
||||
.reply-trigger-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
|
||||
.is-replied {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
&:has(.is-replied) {
|
||||
background: #f3f4f6;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
/* 回复弹窗 */
|
||||
.reply-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
|
||||
.reply-modal-card {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: relative;
|
||||
|
||||
.reply-textarea {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
bottom: 16rpx;
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 28rpx;
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 84rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.cancel {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
737
pages-sub/merchant/pages/goods/detail.vue
Normal file
737
pages-sub/merchant/pages/goods/detail.vue
Normal file
@@ -0,0 +1,737 @@
|
||||
<template>
|
||||
<view class="merchant-goods-detail" v-if="goodsDetail">
|
||||
<!-- 顶部主图轮播 -->
|
||||
<view class="gallery-wrapper">
|
||||
<swiper
|
||||
class="gallery-swiper"
|
||||
:indicator-dots="false"
|
||||
:autoplay="false"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(img, idx) in galleryImages"
|
||||
:key="idx"
|
||||
@click="previewGallery(idx)"
|
||||
>
|
||||
<image class="swiper-img" :src="img" mode="aspectFill"></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="swiper-indicator" v-if="galleryImages.length > 1">
|
||||
<text>{{ currentSwiperIndex + 1 }} / {{ galleryImages.length }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心信息卡片 -->
|
||||
<view class="info-card core-card">
|
||||
<view class="price-row">
|
||||
<view class="main-price">
|
||||
<text class="symbol">¥</text>
|
||||
<text class="num">{{ goodsDetail.price || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="sub-price" v-if="goodsDetail.cost">
|
||||
<text>成本价 ¥{{ goodsDetail.cost }}</text>
|
||||
</view>
|
||||
<view class="status-tags">
|
||||
<view
|
||||
class="badge market"
|
||||
:class="goodsDetail.marketEnable === 'UPPER' ? 'on-sale' : 'off-sale'"
|
||||
>
|
||||
{{ goodsDetail.marketEnable === 'UPPER' ? '出售中' : '已下架' }}
|
||||
</view>
|
||||
<view
|
||||
class="badge auth"
|
||||
:class="goodsDetail.authFlag === 'PASS' ? 'auth-pass' : 'auth-pending'"
|
||||
>
|
||||
{{ getAuthText(goodsDetail.authFlag) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="title-row">
|
||||
<text class="goods-title">{{ goodsDetail.goodsName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="meta-stats-grid">
|
||||
<view class="stat-col">
|
||||
<text class="stat-num">{{ goodsDetail.quantity ?? 0 }}</text>
|
||||
<text class="stat-label">总库存</text>
|
||||
</view>
|
||||
<view class="stat-sep"></view>
|
||||
<view class="stat-col">
|
||||
<text class="stat-num">{{ goodsDetail.buyCount ?? 0 }}</text>
|
||||
<text class="stat-label">累计销量</text>
|
||||
</view>
|
||||
<view class="stat-sep"></view>
|
||||
<view class="stat-col">
|
||||
<text class="stat-num">{{ goodsDetail.sn || '--' }}</text>
|
||||
<text class="stat-label">商品货号</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 规格与多 SKU 列表 -->
|
||||
<view class="info-card" v-if="skuList && skuList.length > 0">
|
||||
<view class="card-head">
|
||||
<view class="head-left">
|
||||
<text class="card-title">规格与 SKU 明细</text>
|
||||
<text class="sku-count-pill">共 {{ skuList.length }} 种规格</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sku-list">
|
||||
<view
|
||||
class="sku-item"
|
||||
v-for="(sku, index) in skuList"
|
||||
:key="sku.id || sku.sn || index"
|
||||
>
|
||||
<image
|
||||
class="sku-thumb"
|
||||
:src="getSkuThumb(sku)"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="sku-info">
|
||||
<text class="sku-spec">{{ formatSpecs(sku, index) }}</text>
|
||||
<text class="sku-sn" v-if="sku.sn">SKU编码:{{ sku.sn }}</text>
|
||||
<view class="sku-nums">
|
||||
<text class="sku-price">售价:<text class="val">¥{{ sku.price || '0.00' }}</text></text>
|
||||
<text class="sku-stock">库存:<text class="val">{{ sku.quantity ?? sku.stock ?? sku.enableQuantity ?? 0 }}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本属性卡片(优化分类展示) -->
|
||||
<view class="info-card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">基本属性</text>
|
||||
</view>
|
||||
<view class="prop-row" v-if="goodsDetail.categoryName">
|
||||
<text class="prop-label">所属分类</text>
|
||||
<text class="prop-val cat-val">{{ formatCategoryName(goodsDetail.categoryName) }}</text>
|
||||
</view>
|
||||
<view class="prop-row" v-if="goodsDetail.brandName">
|
||||
<text class="prop-label">商品品牌</text>
|
||||
<text class="prop-val">{{ goodsDetail.brandName }}</text>
|
||||
</view>
|
||||
<view class="prop-row" v-if="goodsDetail.goodsUnit">
|
||||
<text class="prop-label">计量单位</text>
|
||||
<text class="prop-val">{{ goodsDetail.goodsUnit }}</text>
|
||||
</view>
|
||||
<view class="prop-row" v-if="goodsDetail.releaseTime">
|
||||
<text class="prop-label">发布时间</text>
|
||||
<text class="prop-val">{{ goodsDetail.releaseTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图文详情 -->
|
||||
<view class="info-card intro-card" v-if="goodsDetail.intro || goodsDetail.mobileIntro">
|
||||
<view class="card-head">
|
||||
<text class="card-title">商品图文详情</text>
|
||||
</view>
|
||||
<view class="intro-html">
|
||||
<rich-text :nodes="formatRichText(goodsDetail.mobileIntro || goodsDetail.intro)"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定悬浮操作栏(带安全底间距,防止遮挡内容) -->
|
||||
<view class="bottom-action-bar">
|
||||
<view class="preview-btn" @click="previewInMall">
|
||||
<u-icon name="eye" size="20" color="#4b5563"></u-icon>
|
||||
<text>买家预览</text>
|
||||
</view>
|
||||
<view
|
||||
class="market-action-btn off"
|
||||
v-if="goodsDetail.marketEnable === 'UPPER'"
|
||||
@click="handleToggleMarket('DOWN')"
|
||||
>
|
||||
<text>下架商品</text>
|
||||
</view>
|
||||
<view
|
||||
class="market-action-btn on"
|
||||
v-else
|
||||
@click="handleToggleMarket('UPPER')"
|
||||
>
|
||||
<text>立即上架</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getStoreGoodsDetail, getStoreGoodsSkuList, upGoods, downGoods } from '../../api/goods.js'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const getSkuThumb = (sku: any) => {
|
||||
const img = sku.thumbnail || sku.original
|
||||
if (img) {
|
||||
const parsed = parseGoodsImageUrl(img)
|
||||
if (parsed) return parsed
|
||||
}
|
||||
const rawSpec = sku.simpleSpecs || sku.specs
|
||||
if (rawSpec && typeof rawSpec === 'string') {
|
||||
const match = rawSpec.match(/https?:\/\/[^\s"'\\}]+\.(jpeg|jpg|png|webp|gif)/i)
|
||||
if (match) return match[0]
|
||||
}
|
||||
return galleryImages.value[0] || '/static/tabbar/home.png'
|
||||
}
|
||||
|
||||
|
||||
const goodsId = ref('')
|
||||
const goodsDetail = ref<any>(null)
|
||||
const skuList = ref<any[]>([])
|
||||
const currentSwiperIndex = ref(0)
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options?.id) {
|
||||
goodsId.value = options.id
|
||||
fetchGoodsDetail()
|
||||
}
|
||||
})
|
||||
|
||||
const galleryImages = computed(() => {
|
||||
if (!goodsDetail.value) return []
|
||||
if (Array.isArray(goodsDetail.value.goodsGalleryList) && goodsDetail.value.goodsGalleryList.length > 0) {
|
||||
return goodsDetail.value.goodsGalleryList
|
||||
}
|
||||
const defaultImg = goodsDetail.value.original || goodsDetail.value.thumbnail
|
||||
return defaultImg ? [defaultImg] : ['/static/tabbar/home.png']
|
||||
})
|
||||
|
||||
const fetchGoodsDetail = async () => {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
try {
|
||||
const res: any = await getStoreGoodsDetail(goodsId.value)
|
||||
uni.hideLoading()
|
||||
if (res?.data && res.data.success && res.data.result) {
|
||||
goodsDetail.value = res.data.result
|
||||
skuList.value = res.data.result.skuList || []
|
||||
if (skuList.value.length === 0) {
|
||||
fetchSkuList()
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '获取商品详情失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSkuList = async () => {
|
||||
try {
|
||||
const skuRes: any = await getStoreGoodsSkuList(goodsId.value)
|
||||
if (skuRes?.data && skuRes.data.success && Array.isArray(skuRes.data.result)) {
|
||||
skuList.value = skuRes.data.result
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const onSwiperChange = (e: any) => {
|
||||
currentSwiperIndex.value = e.detail.current
|
||||
}
|
||||
|
||||
const previewGallery = (current: number) => {
|
||||
uni.previewImage({
|
||||
urls: galleryImages.value,
|
||||
current,
|
||||
})
|
||||
}
|
||||
|
||||
// 智能清洗并优雅格式化规格文本(过滤 images 等系统 JSON 冗余)
|
||||
const formatSpecs = (sku: any, index: number) => {
|
||||
// 1. 优先使用规范的 specList 数组
|
||||
if (sku.specList && Array.isArray(sku.specList) && sku.specList.length > 0) {
|
||||
const listStr = sku.specList
|
||||
.map((s: any) => {
|
||||
if (s.specName && s.specValue) return `${s.specName}: ${s.specValue}`
|
||||
return s.specValue || s.specName
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
if (listStr) return listStr
|
||||
}
|
||||
|
||||
// 2. 深度清洗 simpleSpecs 或 specs 中的复杂复合 JSON
|
||||
const rawSpec = sku.simpleSpecs || sku.specs
|
||||
if (rawSpec && typeof rawSpec === 'string') {
|
||||
const trimmed = rawSpec.trim()
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
const obj = JSON.parse(trimmed)
|
||||
const ignoreKeys = ['images', 'code', 'message', 'result', 'success', 'showProgress', 'url', 'status', 'specImage', 'specType']
|
||||
const pairs: string[] = []
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (!ignoreKeys.includes(k) && typeof v === 'string' && !v.startsWith('http') && v.length < 50) {
|
||||
pairs.push(`${k}: ${v}`)
|
||||
}
|
||||
}
|
||||
if (pairs.length > 0) return pairs.join(' / ')
|
||||
} catch (e) {}
|
||||
|
||||
// JSON 解析失败时正则提取真实的规格键值对
|
||||
const matches = [...trimmed.matchAll(/"([^"]{1,20})"\s*:\s*"([^"]{1,30})"/g)]
|
||||
const ignoreKeys = ['code', 'message', 'success', 'url', 'result', 'showProgress', 'status', 'name']
|
||||
const pairs: string[] = []
|
||||
for (const m of matches) {
|
||||
const k = m[1]
|
||||
const v = m[2]
|
||||
if (!ignoreKeys.includes(k) && !v.startsWith('http')) {
|
||||
pairs.push(`${k}: ${v}`)
|
||||
}
|
||||
}
|
||||
if (pairs.length > 0) return pairs.join(' / ')
|
||||
} else {
|
||||
// 纯文本规格
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 提取不同于 SPU 主标题的商品名称差异
|
||||
if (sku.goodsName && sku.goodsName !== goodsDetail.value?.goodsName) {
|
||||
return sku.goodsName
|
||||
}
|
||||
|
||||
// 4. 最终兜底
|
||||
return skuList.value.length > 1 ? `规格 ${index + 1}` : '统一规格'
|
||||
}
|
||||
|
||||
// 格式化分类展示:将原始 JSON 数组 [ "数码办公", "手机通讯", "手机" ] 转为层级面包屑 "数码办公 > 手机通讯 > 手机"
|
||||
const formatCategoryName = (cat: any) => {
|
||||
if (!cat) return '--'
|
||||
if (Array.isArray(cat)) {
|
||||
return cat.filter(Boolean).join(' > ')
|
||||
}
|
||||
if (typeof cat === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(cat)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.filter(Boolean).join(' > ')
|
||||
}
|
||||
} catch (e) {}
|
||||
// 处理带引号或逗号的非标准字符串
|
||||
return cat.replace(/[\[\]"']/g, '').split(',').map((s: string) => s.trim()).filter(Boolean).join(' > ')
|
||||
}
|
||||
return String(cat)
|
||||
}
|
||||
|
||||
const getAuthText = (flag: string) => {
|
||||
const map: Record<string, string> = {
|
||||
PASS: '审核通过',
|
||||
AUDIT: '待审核',
|
||||
REFUSE: '审核驳回',
|
||||
}
|
||||
return map[flag] || flag || '已通过'
|
||||
}
|
||||
|
||||
const formatRichText = (html: string) => {
|
||||
if (!html) return ''
|
||||
return html
|
||||
.replace(/<img[^>]*>/gi, (match) => {
|
||||
return match
|
||||
.replace(/style="[^"]*"/gi, '')
|
||||
.replace(/width="[^"]*"/gi, '')
|
||||
.replace(/height="[^"]*"/gi, '')
|
||||
})
|
||||
.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;border-radius:12rpx;margin:16rpx 0;"')
|
||||
}
|
||||
|
||||
// 上下架切换
|
||||
const handleToggleMarket = (targetStatus: string) => {
|
||||
const isUp = targetStatus === 'UPPER'
|
||||
const actionText = isUp ? '上架' : '下架'
|
||||
|
||||
uni.showModal({
|
||||
title: `${actionText}商品`,
|
||||
content: `确定要${actionText}当前商品?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: `正在${actionText}...` })
|
||||
try {
|
||||
const apiCall = isUp ? upGoods(goodsId.value) : downGoods(goodsId.value)
|
||||
const resp: any = await apiCall
|
||||
uni.hideLoading()
|
||||
if (resp?.data && resp.data.success) {
|
||||
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
||||
fetchGoodsDetail()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: resp?.data?.message || `${actionText}失败`,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 前往买家端预览
|
||||
const previewInMall = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${goodsId.value}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-goods-detail {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f5f7;
|
||||
/* 预留出底部固定操作栏的高以及全面屏安全区,彻底解决内容被底部遮挡 */
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.gallery-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 680rpx;
|
||||
background: #ffffff;
|
||||
|
||||
.gallery-swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.swiper-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-indicator {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
bottom: 44rpx;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: 20rpx 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f9fafb;
|
||||
|
||||
.head-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.sku-count-pill {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.core-card {
|
||||
margin-top: -30rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
.main-price {
|
||||
color: #ef4444;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.symbol {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-price {
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
margin-left: 16rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tags {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
|
||||
.badge {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.on-sale {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.off-sale {
|
||||
background: #f3f4f6;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&.auth-pass {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
&.auth-pending {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-row {
|
||||
margin-top: 20rpx;
|
||||
|
||||
.goods-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-stats-grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
border-radius: 16rpx;
|
||||
padding: 22rpx 0;
|
||||
margin-top: 26rpx;
|
||||
|
||||
.stat-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.stat-num {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-sep {
|
||||
width: 1rpx;
|
||||
height: 38rpx;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sku-list {
|
||||
.sku-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f3f4f6;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sku-thumb {
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #f3f4f6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sku-info {
|
||||
flex: 1;
|
||||
margin-left: 22rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sku-spec {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sku-sn {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.sku-nums {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-top: 10rpx;
|
||||
|
||||
.sku-price {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
|
||||
.val {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-stock {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
|
||||
.val {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prop-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid #f9fafb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.prop-label {
|
||||
font-size: 26rpx;
|
||||
color: #6b7280;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.prop-val {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 26rpx;
|
||||
color: #111827;
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
|
||||
&.cat-val {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
display: inline-block;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.intro-card {
|
||||
.intro-html {
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx calc(16rpx + env(safe-area-inset-bottom)) 28rpx;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
z-index: 100;
|
||||
gap: 20rpx;
|
||||
|
||||
.preview-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20rpx;
|
||||
|
||||
text {
|
||||
font-size: 20rpx;
|
||||
color: #4b5563;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.market-action-btn {
|
||||
flex: 1;
|
||||
height: 84rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.off {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1rpx solid #fecaca;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
472
pages-sub/merchant/pages/goods/list.vue
Normal file
472
pages-sub/merchant/pages/goods/list.vue
Normal file
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<view class="merchant-goods-page">
|
||||
<!-- 顶部吸顶搜索栏与 Tab -->
|
||||
<view class="sticky-header">
|
||||
<view class="search-bar">
|
||||
<u-icon name="search" size="18" color="#9ca3af"></u-icon>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="keywords"
|
||||
placeholder="搜索商品名称"
|
||||
placeholder-class="placeholder"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<view class="clear-btn" v-if="keywords" @click="clearSearch">
|
||||
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tab-scroll-wrap">
|
||||
<view
|
||||
class="tab-item"
|
||||
v-for="item in tabList"
|
||||
:key="item.key"
|
||||
:class="{ active: currentTab === item.key }"
|
||||
@click="switchTab(item.key)"
|
||||
>
|
||||
<text class="tab-label">{{ item.label }}</text>
|
||||
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表主体 -->
|
||||
<view class="goods-list-body">
|
||||
<view
|
||||
class="goods-card"
|
||||
v-for="goods in goodsList"
|
||||
:key="goods.id"
|
||||
@click="goToDetail(goods.id)"
|
||||
>
|
||||
<view class="card-main">
|
||||
<image
|
||||
class="goods-img"
|
||||
:src="goods.thumbnail || goods.original || '/static/tabbar/home.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="goods-meta">
|
||||
<view class="title-row">
|
||||
<text class="goods-title">{{ goods.goodsName }}</text>
|
||||
</view>
|
||||
<view class="stock-sales-row">
|
||||
<text class="meta-tag">库存: {{ goods.quantity ?? 0 }}</text>
|
||||
<text class="meta-tag">销量: {{ goods.buyCount ?? 0 }}</text>
|
||||
</view>
|
||||
<view class="price-status-row">
|
||||
<view class="price-box">
|
||||
<text class="symbol">¥</text>
|
||||
<text class="price-num">{{ goods.price || goods.sellingPrice || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="status-badge" :class="goods.marketEnable === 'UPPER' ? 'on-sale' : 'off-sale'">
|
||||
{{ goods.marketEnable === 'UPPER' ? '出售中' : '已下架' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部快捷操作条 -->
|
||||
<view class="card-footer">
|
||||
<text class="goods-sn" v-if="goods.sn">商品货号:{{ goods.sn }}</text>
|
||||
<view class="actions-group">
|
||||
<view
|
||||
class="action-btn off"
|
||||
v-if="goods.marketEnable === 'UPPER'"
|
||||
@click.stop="toggleMarket(goods, 'DOWN')"
|
||||
>
|
||||
<text>下架商品</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-btn on"
|
||||
v-else
|
||||
@click.stop="toggleMarket(goods, 'UPPER')"
|
||||
>
|
||||
<text>立即上架</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && goodsList.length === 0">
|
||||
<u-icon name="bag" size="56" color="#d1d5db"></u-icon>
|
||||
<text class="empty-text">暂无相关商品</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="loading-more" v-if="loading">
|
||||
<text>正在加载商品列表...</text>
|
||||
</view>
|
||||
<view class="no-more" v-else-if="finished && goodsList.length > 0">
|
||||
<text>没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getStoreGoodsList, upGoods, downGoods } from '../../api/goods.js'
|
||||
|
||||
const keywords = ref('')
|
||||
const currentTab = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const goodsList = ref([])
|
||||
|
||||
const tabList = [
|
||||
{ key: 'ALL', label: '全部' },
|
||||
{ key: 'UPPER', label: '出售中' },
|
||||
{ key: 'DOWN', label: '已下架' },
|
||||
]
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.tab) {
|
||||
currentTab.value = options.tab
|
||||
}
|
||||
fetchGoods(true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchGoods(true)
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!finished.value && !loading.value) {
|
||||
pageNumber.value++
|
||||
fetchGoods(false)
|
||||
}
|
||||
})
|
||||
|
||||
const fetchGoods = async (reset = false) => {
|
||||
if (reset) {
|
||||
pageNumber.value = 1
|
||||
goodsList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const params = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
|
||||
if (keywords.value.trim()) {
|
||||
params.goodsName = keywords.value.trim()
|
||||
}
|
||||
|
||||
if (currentTab.value !== 'ALL') {
|
||||
params.marketEnable = currentTab.value
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getStoreGoodsList(params)
|
||||
loading.value = false
|
||||
if (res.data && res.data.success && res.data.result) {
|
||||
const records = res.data.result.records || []
|
||||
if (reset) {
|
||||
goodsList.value = records
|
||||
} else {
|
||||
goodsList.value = [...goodsList.value, ...records]
|
||||
}
|
||||
if (records.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
} else {
|
||||
finished.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (tabKey) => {
|
||||
if (currentTab.value === tabKey) return
|
||||
currentTab.value = tabKey
|
||||
fetchGoods(true)
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
fetchGoods(true)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
keywords.value = ''
|
||||
fetchGoods(true)
|
||||
}
|
||||
|
||||
const goToDetail = (id) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages-sub/merchant/pages/goods/detail?id=${id}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 快捷上下架切换
|
||||
const toggleMarket = (goods, targetStatus) => {
|
||||
const isUp = targetStatus === 'UPPER'
|
||||
const actionText = isUp ? '上架' : '下架'
|
||||
|
||||
uni.showModal({
|
||||
title: `${actionText}提示`,
|
||||
content: `确定要${actionText}商品「${goods.goodsName}」吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: `正在${actionText}...` })
|
||||
try {
|
||||
const apiCall = isUp ? upGoods(goods.id) : downGoods(goods.id)
|
||||
const resp = await apiCall
|
||||
uni.hideLoading()
|
||||
if (resp.data && resp.data.success) {
|
||||
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
||||
fetchGoods(true)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: resp.data?.message || `${actionText}失败`,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-goods-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f3f4f6;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f3f4f6;
|
||||
margin: 20rpx 28rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-scroll-wrap {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding: 0 16rpx;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
position: relative;
|
||||
|
||||
.tab-label {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-label {
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
background: #111827;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list-body {
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
.goods-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
.card-main {
|
||||
display: flex;
|
||||
|
||||
.goods-img {
|
||||
width: 170rpx;
|
||||
height: 170rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.goods-meta {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.title-row {
|
||||
.goods-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.stock-sales-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
|
||||
.meta-tag {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.price-status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #ff3c2a;
|
||||
|
||||
.symbol {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.on-sale {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.off-sale {
|
||||
background: #f3f4f6;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f9fafb;
|
||||
|
||||
.goods-sn {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.actions-group {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.action-btn {
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.off {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1rpx solid #fecaca;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
812
pages-sub/merchant/pages/index/index.vue
Normal file
812
pages-sub/merchant/pages/index/index.vue
Normal file
@@ -0,0 +1,812 @@
|
||||
<template>
|
||||
<view class="workbench-page">
|
||||
<!-- 顶部商家信息栏 -->
|
||||
<view class="store-hero">
|
||||
<view class="store-info-row">
|
||||
<view class="store-avatar">
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="storeInfo.settings?.storeLogo || '/static/tabbar/home.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="store-meta">
|
||||
<view class="name-row">
|
||||
<text class="store-name">{{ storeInfo.settings?.storeName || storeInfo.store?.storeName || '商家移动工作台' }}</text>
|
||||
<view class="badge" :class="isClerk ? 'clerk-badge' : 'owner-badge'">
|
||||
{{ isClerk ? '店员' : '店主' }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="clerk-name">当前操作员:{{ operatorName }}</text>
|
||||
</view>
|
||||
<view class="logout-btn" @click="handleLogout">
|
||||
<text>退出</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心操作区(快速核销) -->
|
||||
<view class="core-actions-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">快速核销</text>
|
||||
<text class="card-subtitle">支持预约单、自提单与虚拟卡密</text>
|
||||
</view>
|
||||
|
||||
<!-- 扫码主按钮 -->
|
||||
<view class="scan-hero-btn" @click="handleScanCode">
|
||||
<view class="scan-icon-circle">
|
||||
<u-icon name="scan" size="38" color="#ffffff"></u-icon>
|
||||
</view>
|
||||
<view class="scan-text-box">
|
||||
<text class="main-text">扫码快速核销</text>
|
||||
<text class="sub-text">扫描顾客出示的核销码/订单码</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" size="18" color="rgba(255,255,255,0.6)"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 手动输码 -->
|
||||
<view class="manual-input-box">
|
||||
<view class="input-wrap">
|
||||
<u-icon name="edit-pen" size="20" color="#9ca3af"></u-icon>
|
||||
<input
|
||||
class="code-input"
|
||||
type="number"
|
||||
maxlength="12"
|
||||
v-model="manualCode"
|
||||
placeholder="输入8位数字核销码"
|
||||
placeholder-class="placeholder"
|
||||
@confirm="handleManualVerify"
|
||||
/>
|
||||
<view class="clear-icon" v-if="manualCode" @click="clearManualCode">
|
||||
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="verify-btn"
|
||||
:class="{ active: manualCode.trim().length >= 6 }"
|
||||
@click="handleManualVerify"
|
||||
>
|
||||
<text>核销</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心业务四大板块 -->
|
||||
<view class="business-grid-card">
|
||||
<view class="grid-card-header">
|
||||
<text class="grid-card-title">经营管理</text>
|
||||
</view>
|
||||
<view class="business-grid">
|
||||
<!-- 订单管理 -->
|
||||
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/order/list?tab=ALL')">
|
||||
<view class="biz-icon-box bg-blue">
|
||||
<u-icon name="order" size="26" color="#2563eb"></u-icon>
|
||||
</view>
|
||||
<text class="biz-title">订单管理</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品管理 -->
|
||||
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/goods/list')">
|
||||
<view class="biz-icon-box bg-emerald">
|
||||
<u-icon name="bag" size="26" color="#059669"></u-icon>
|
||||
</view>
|
||||
<text class="biz-title">商品管理</text>
|
||||
</view>
|
||||
|
||||
<!-- 评价管理 -->
|
||||
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/evaluation/list')">
|
||||
<view class="biz-icon-box bg-amber">
|
||||
<u-icon name="chat" size="26" color="#d97706"></u-icon>
|
||||
</view>
|
||||
<text class="biz-title">评价管理</text>
|
||||
</view>
|
||||
|
||||
<!-- 待核销专区 -->
|
||||
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/order/list?tab=TAKE')">
|
||||
<view class="biz-icon-box bg-orange">
|
||||
<u-icon name="checkmark-circle" size="26" color="#ea580c"></u-icon>
|
||||
</view>
|
||||
<text class="biz-title">待核销单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 核销确认弹窗 -->
|
||||
<view class="modal-mask" v-if="showModal" @click.self="closeModal">
|
||||
<view class="modal-card">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">核销信息确认</text>
|
||||
<view class="close-btn" @click="closeModal">
|
||||
<u-icon name="close" size="18" color="#9ca3af"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单概览 -->
|
||||
<view class="order-summary" v-if="verifyingOrder">
|
||||
<view class="summary-row">
|
||||
<text class="label">核销码:</text>
|
||||
<text class="value highlight-code">{{ currentCode }}</text>
|
||||
</view>
|
||||
<view class="summary-row">
|
||||
<text class="label">订单编号:</text>
|
||||
<text class="value">{{ verifyingOrder.sn || verifyingOrder.orderSn || '--' }}</text>
|
||||
</view>
|
||||
<view class="summary-row" v-if="verifyingOrder.consigneeMobile || verifyingOrder.memberPhone">
|
||||
<text class="label">顾客电话:</text>
|
||||
<text class="value">{{ maskPhone(verifyingOrder.consigneeMobile || verifyingOrder.memberPhone) }}</text>
|
||||
</view>
|
||||
<view class="summary-row" v-if="verifyingOrder.goodsName || verifyingOrder.orderItems?.[0]?.goodsName">
|
||||
<text class="label">商品名称:</text>
|
||||
<text class="value goods-text">
|
||||
{{ verifyingOrder.goodsName || verifyingOrder.orderItems?.[0]?.goodsName }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="summary-row" v-if="verifyingOrder.num">
|
||||
<text class="label">购买数量:</text>
|
||||
<text class="value">{{ verifyingOrder.num }} 件</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约服务专属选项:选择服务人员 -->
|
||||
<view class="staff-picker-box" v-if="isAppointmentOrder">
|
||||
<text class="staff-label">指派/核销服务人员:</text>
|
||||
<picker
|
||||
class="picker-ctrl"
|
||||
mode="selector"
|
||||
:range="staffList"
|
||||
range-key="name"
|
||||
:value="selectedStaffIndex"
|
||||
@change="onStaffChange"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text>{{ currentStaffName }}</text>
|
||||
<u-icon name="arrow-down" size="14" color="#6b7280"></u-icon>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗操作 -->
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeModal">取消</view>
|
||||
<view
|
||||
class="modal-btn confirm"
|
||||
:class="{ loading: confirming }"
|
||||
@click="submitConfirmVerification"
|
||||
>
|
||||
{{ confirming ? '核销中...' : '确认核销' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import storage from '@/utils/storage.js'
|
||||
import {
|
||||
previewStoreVerification,
|
||||
storeOrderTake,
|
||||
confirmStoreVerification,
|
||||
getAppointmentOrderByCode,
|
||||
verifyAppointmentCode,
|
||||
getEnabledServiceStaff,
|
||||
} from '../../api/order.js'
|
||||
import { storeLogout } from '../../api/passport.js'
|
||||
|
||||
const storeInfo = ref({})
|
||||
|
||||
const operatorName = computed(() => {
|
||||
const u = storeInfo.value?.user
|
||||
return u?.nickName || u?.username || '当前操作员'
|
||||
})
|
||||
|
||||
const isClerk = computed(() => {
|
||||
const u = storeInfo.value?.user
|
||||
if (u && (u.isSuper === false || u.shopkeeper === false)) return true
|
||||
return false
|
||||
})
|
||||
const manualCode = ref('')
|
||||
const showModal = ref(false)
|
||||
const confirming = ref(false)
|
||||
const currentCode = ref('')
|
||||
const verifyingOrder = ref(null)
|
||||
const isAppointmentOrder = ref(false)
|
||||
|
||||
const staffList = ref([])
|
||||
const selectedStaffIndex = ref(0)
|
||||
|
||||
const currentStaffName = computed(() => {
|
||||
if (staffList.value.length > 0 && staffList.value[selectedStaffIndex.value]) {
|
||||
return staffList.value[selectedStaffIndex.value].name
|
||||
}
|
||||
return '未指派 / 默认'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const token = storage.getStoreAccessToken()
|
||||
if (!token) {
|
||||
uni.redirectTo({ url: '/pages-sub/merchant/pages/login/index' })
|
||||
return
|
||||
}
|
||||
storeInfo.value = storage.getStoreInfo() || {}
|
||||
loadStaffList()
|
||||
})
|
||||
|
||||
const loadStaffList = async () => {
|
||||
try {
|
||||
const res = await getEnabledServiceStaff()
|
||||
if (res.data && res.data.success && Array.isArray(res.data.result)) {
|
||||
staffList.value = res.data.result
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略加载异常
|
||||
}
|
||||
}
|
||||
|
||||
const maskPhone = (phone) => {
|
||||
if (!phone || phone.length < 7) return phone || ''
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
// 扫码快速核销
|
||||
const handleScanCode = () => {
|
||||
uni.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
let code = res.result || ''
|
||||
if (code.includes('code=')) {
|
||||
const match = code.match(/code=([a-zA-Z0-9]+)/)
|
||||
if (match && match[1]) {
|
||||
code = match[1]
|
||||
}
|
||||
}
|
||||
if (code) {
|
||||
queryAndShowVerify(code.trim())
|
||||
} else {
|
||||
uni.showToast({ title: '未能识别核销码', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
// 手动输码核销
|
||||
const clearManualCode = () => {
|
||||
manualCode.value = ''
|
||||
}
|
||||
|
||||
const handleManualVerify = () => {
|
||||
const code = manualCode.value.trim()
|
||||
if (!code || code.length < 6) {
|
||||
uni.showToast({ title: '请输入有效的核销码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
queryAndShowVerify(code)
|
||||
}
|
||||
|
||||
// 预检核销码并展示弹窗
|
||||
const queryAndShowVerify = async (code) => {
|
||||
currentCode.value = code
|
||||
uni.showLoading({ title: '正在查询订单...' })
|
||||
|
||||
try {
|
||||
let isAppoint = false
|
||||
let orderData = null
|
||||
|
||||
try {
|
||||
const appointRes = await getAppointmentOrderByCode(code)
|
||||
if (appointRes.data && appointRes.data.success && appointRes.data.result) {
|
||||
isAppoint = true
|
||||
orderData = appointRes.data.result
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (!orderData) {
|
||||
const previewRes = await previewStoreVerification(code)
|
||||
if (previewRes.data && previewRes.data.success && previewRes.data.result) {
|
||||
orderData = previewRes.data.result
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (orderData) {
|
||||
verifyingOrder.value = orderData
|
||||
isAppointmentOrder.value = isAppoint
|
||||
showModal.value = true
|
||||
} else {
|
||||
verifyingOrder.value = { sn: '以提交核销为准' }
|
||||
isAppointmentOrder.value = false
|
||||
showModal.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
verifyingOrder.value = { sn: '以提交核销为准' }
|
||||
isAppointmentOrder.value = false
|
||||
showModal.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const onStaffChange = (e) => {
|
||||
selectedStaffIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false
|
||||
verifyingOrder.value = null
|
||||
manualCode.value = ''
|
||||
}
|
||||
|
||||
// 确认核销提交
|
||||
const submitConfirmVerification = async () => {
|
||||
if (confirming.value) return
|
||||
confirming.value = true
|
||||
uni.showLoading({ title: '核销中...' })
|
||||
|
||||
try {
|
||||
let success = false
|
||||
let message = ''
|
||||
|
||||
if (isAppointmentOrder.value) {
|
||||
const staff = staffList.value[selectedStaffIndex.value]
|
||||
const params = {
|
||||
orderSn: verifyingOrder.value?.orderSn || verifyingOrder.value?.sn,
|
||||
code: currentCode.value,
|
||||
serviceStaffId: staff ? staff.id : undefined,
|
||||
}
|
||||
const res = await verifyAppointmentCode(params)
|
||||
success = res.data && res.data.success
|
||||
message = res.data?.message
|
||||
} else {
|
||||
const sn = verifyingOrder.value?.orderSn || verifyingOrder.value?.sn
|
||||
if (sn && sn !== '以提交核销为准') {
|
||||
const res = await storeOrderTake(sn, currentCode.value)
|
||||
success = res.data && res.data.success
|
||||
message = res.data?.message
|
||||
} else {
|
||||
const res = await confirmStoreVerification(currentCode.value)
|
||||
success = res.data && res.data.success
|
||||
message = res.data?.message
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
confirming.value = false
|
||||
|
||||
if (success) {
|
||||
try {
|
||||
uni.vibrateShort()
|
||||
} catch (e) {}
|
||||
uni.showToast({ title: '核销成功!', icon: 'success' })
|
||||
closeModal()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: message || '核销失败,请核对核销码',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
confirming.value = false
|
||||
uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: '退出商家工作台',
|
||||
content: '退出后将清除本店登录凭证,确认退出?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await storeLogout()
|
||||
} catch (e) {}
|
||||
storage.clearStoreAuth()
|
||||
uni.redirectTo({
|
||||
url: '/pages-sub/merchant/pages/login/index',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workbench-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f3f4f6;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.store-hero {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||
padding: 60rpx 36rpx 100rpx 36rpx;
|
||||
color: #ffffff;
|
||||
|
||||
.store-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.store-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.store-meta {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.store-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.owner-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.clerk-badge {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clerk-name {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 10rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-actions-card {
|
||||
margin: -60rpx 32rpx 28rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 6rpx;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-hero-btn {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border-radius: 22rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 10rpx 24rpx rgba(16, 185, 129, 0.3);
|
||||
|
||||
.scan-icon-circle {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scan-text-box {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
|
||||
.main-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 6rpx;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manual-input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 28rpx;
|
||||
gap: 16rpx;
|
||||
|
||||
.input-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
padding: 18rpx 24rpx;
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.verify-btn {
|
||||
padding: 20rpx 36rpx;
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.active {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.business-grid-card {
|
||||
margin: 0 32rpx 24rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.grid-card-header {
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 8rpx;
|
||||
|
||||
.grid-card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.business-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.biz-item {
|
||||
width: 50%;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.biz-icon-box {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&.bg-blue {
|
||||
background: #eff6ff;
|
||||
}
|
||||
&.bg-emerald {
|
||||
background: #ecfdf5;
|
||||
}
|
||||
&.bg-amber {
|
||||
background: #fffbeb;
|
||||
}
|
||||
&.bg-orange {
|
||||
background: #fff7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.biz-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.biz-sub {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* 弹窗遮罩 */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
|
||||
.modal-card {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
border-radius: 28rpx;
|
||||
padding: 36rpx;
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background: #f9fafb;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 14rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #6b7280;
|
||||
width: 150rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #111827;
|
||||
flex: 1;
|
||||
|
||||
&.highlight-code {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.goods-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.staff-picker-box {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx dashed #e5e7eb;
|
||||
|
||||
.staff-label {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.picker-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #d1d5db;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 84rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.cancel {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
550
pages-sub/merchant/pages/login/index.vue
Normal file
550
pages-sub/merchant/pages/login/index.vue
Normal file
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<view class="merchant-login-page">
|
||||
<!-- 顶部状态栏沉浸背景 -->
|
||||
<view class="header-bg">
|
||||
<view class="brand-box">
|
||||
<view class="badge-icon">
|
||||
<u-icon name="bag" size="32" color="#ffffff"></u-icon>
|
||||
</view>
|
||||
<text class="brand-title">商家管理工作台</text>
|
||||
<text class="brand-desc">随时随地 · 快捷扫码核销与订单管理</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<!-- 模式切换 Tab -->
|
||||
<view class="tab-header">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: loginType === 'account' }"
|
||||
@click="switchTab('account')"
|
||||
>
|
||||
<text>账号密码登录</text>
|
||||
<view class="active-indicator" v-if="loginType === 'account'"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: loginType === 'sms' }"
|
||||
@click="switchTab('sms')"
|
||||
>
|
||||
<text>短信验证码登录</text>
|
||||
<view class="active-indicator" v-if="loginType === 'sms'"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号密码表单 -->
|
||||
<view class="form-container" v-if="loginType === 'account'">
|
||||
<view class="input-item">
|
||||
<u-input
|
||||
v-model="accountForm.username"
|
||||
placeholder="请输入商家账号/手机号"
|
||||
border="surround"
|
||||
prefixIcon="account"
|
||||
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||
:customStyle="customInputStyle"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-item">
|
||||
<u-input
|
||||
v-model="accountForm.password"
|
||||
placeholder="请输入登录密码"
|
||||
type="password"
|
||||
border="surround"
|
||||
prefixIcon="lock"
|
||||
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||
:customStyle="customInputStyle"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 短信验证码表单 -->
|
||||
<view class="form-container" v-else>
|
||||
<view class="input-item">
|
||||
<u-input
|
||||
v-model="smsForm.mobile"
|
||||
placeholder="请输入店主手机号"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="surround"
|
||||
prefixIcon="phone"
|
||||
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||
:customStyle="customInputStyle"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-item">
|
||||
<u-input
|
||||
v-model="smsForm.code"
|
||||
placeholder="请输入 6 位短信验证码"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="surround"
|
||||
prefixIcon="email"
|
||||
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||
:customStyle="customInputStyle"
|
||||
clearable
|
||||
>
|
||||
<template #suffix>
|
||||
<view
|
||||
class="send-btn"
|
||||
:class="{ disabled: smsCount > 0 || !smsForm.mobile }"
|
||||
@click="handleSendSms"
|
||||
>
|
||||
<text>{{ smsCount > 0 ? `${smsCount}s后重试` : '获取验证码' }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</u-input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录提交按钮 -->
|
||||
<view class="action-btn" :class="{ loading: submitting }" @click="handleLogin">
|
||||
<text class="btn-text">{{ submitting ? '正在登录...' : '立即进入工作台' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="notice-tip">
|
||||
<u-icon name="info-circle" size="14" color="#8c8c8c"></u-icon>
|
||||
<text class="notice-text">仅限已入驻商城的店主/店员账号登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 安全滑块拼图验证码弹窗 -->
|
||||
<myVerification
|
||||
v-if="codeFlag"
|
||||
ref="verificationRef"
|
||||
apiType="store"
|
||||
business="LOGIN"
|
||||
@send="handleVerificationSuccess"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import storage from '@/utils/storage.js'
|
||||
import myVerification from '@/components/verification/verification.vue'
|
||||
import {
|
||||
userLogin,
|
||||
smsLogin,
|
||||
sendStoreSms,
|
||||
getStoreSettings,
|
||||
getStoreUserInfo,
|
||||
getStoreDetail,
|
||||
} from '../../api/passport.js'
|
||||
|
||||
const loginType = ref<'account' | 'sms'>('account')
|
||||
const submitting = ref(false)
|
||||
const smsCount = ref(0)
|
||||
let timer: any = null
|
||||
|
||||
// 滑块验证码相关控制
|
||||
const codeFlag = ref(false)
|
||||
const verificationRef = ref<any>(null)
|
||||
const verifyAction = ref<'login' | 'sms'>('login')
|
||||
|
||||
const customInputStyle = {
|
||||
backgroundColor: '#f9fafb',
|
||||
borderRadius: '16rpx',
|
||||
height: '84rpx',
|
||||
paddingLeft: '16rpx',
|
||||
fontSize: '28rpx',
|
||||
}
|
||||
|
||||
const accountForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const smsForm = reactive({
|
||||
mobile: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
// 如果已存在商家 token,直接进入工作台
|
||||
if (storage.getStoreAccessToken()) {
|
||||
uni.redirectTo({
|
||||
url: '/pages-sub/merchant/pages/index/index',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
|
||||
function switchTab(type: 'account' | 'sms') {
|
||||
loginType.value = type
|
||||
}
|
||||
|
||||
// 弹出滑块拼图验证码
|
||||
function openVerification(action: 'login' | 'sms') {
|
||||
verifyAction.value = action
|
||||
codeFlag.value = true
|
||||
nextTick(() => {
|
||||
verificationRef.value?.show()
|
||||
verificationRef.value?.getCode()
|
||||
})
|
||||
}
|
||||
|
||||
// 点击获取短信验证码
|
||||
const handleSendSms = () => {
|
||||
if (smsCount.value > 0) return
|
||||
if (!/^1[3-9]\d{9}$/.test(smsForm.mobile)) {
|
||||
uni.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 发送短信验证码前先通过滑块拼图验证
|
||||
openVerification('sms')
|
||||
}
|
||||
|
||||
// 点击“立即进入工作台”按钮
|
||||
const handleLogin = () => {
|
||||
if (submitting.value) return
|
||||
|
||||
if (loginType.value === 'account') {
|
||||
if (!accountForm.username.trim()) {
|
||||
uni.showToast({ title: '请输入商家账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!accountForm.password) {
|
||||
uni.showToast({ title: '请输入登录密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 账号密码登录:必须先通过安全滑块拼图验证
|
||||
openVerification('login')
|
||||
} else {
|
||||
// 短信验证码登录
|
||||
if (!/^1[3-9]\d{9}$/.test(smsForm.mobile)) {
|
||||
uni.showToast({ title: '请输入有效的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!smsForm.code.trim()) {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 短信验证码登录直接提交
|
||||
doSmsLogin()
|
||||
}
|
||||
}
|
||||
|
||||
// 滑块验证码成功回调
|
||||
const handleVerificationSuccess = () => {
|
||||
if (verifyAction.value === 'login') {
|
||||
doAccountLogin()
|
||||
} else if (verifyAction.value === 'sms') {
|
||||
doSendSms()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行账号密码登录
|
||||
const doAccountLogin = async () => {
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '正在登录...' })
|
||||
|
||||
try {
|
||||
const res: any = await userLogin({
|
||||
username: accountForm.username.trim(),
|
||||
password: accountForm.password,
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
|
||||
if (res?.data && res.data.success && res.data.result) {
|
||||
codeFlag.value = false
|
||||
const { accessToken, refreshToken } = res.data.result
|
||||
storage.setStoreAccessToken(accessToken)
|
||||
if (refreshToken) {
|
||||
storage.setStoreRefreshToken(refreshToken)
|
||||
}
|
||||
|
||||
// 获取店铺与店员信息
|
||||
try {
|
||||
const [settingsRes, userRes, storeRes] = (await Promise.all([
|
||||
getStoreSettings().catch(() => null),
|
||||
getStoreUserInfo().catch(() => null),
|
||||
getStoreDetail().catch(() => null),
|
||||
])) || []
|
||||
|
||||
const storeInfo = {
|
||||
settings: settingsRes?.data?.result || {},
|
||||
user: userRes?.data?.result || {},
|
||||
store: storeRes?.data?.result || {},
|
||||
}
|
||||
storage.setStoreInfo(storeInfo)
|
||||
} catch (e) {
|
||||
console.warn('获取店铺额外信息失败', e)
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages-sub/merchant/pages/index/index',
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res?.data?.message || '登录失败,请检查账号密码',
|
||||
icon: 'none',
|
||||
})
|
||||
verificationRef.value?.getCode()
|
||||
}
|
||||
} catch (err: any) {
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
verificationRef.value?.getCode()
|
||||
uni.showToast({
|
||||
title: err?.data?.message || '网络连接异常或账号密码错误',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 执行发送短信验证码
|
||||
const doSendSms = async () => {
|
||||
try {
|
||||
uni.showLoading({ title: '发送中...' })
|
||||
const res = await sendStoreSms(smsForm.mobile, 'LOGIN')
|
||||
uni.hideLoading()
|
||||
if (res?.data && res.data.success) {
|
||||
codeFlag.value = false
|
||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||
smsCount.value = 60
|
||||
timer = setInterval(() => {
|
||||
smsCount.value--
|
||||
if (smsCount.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
verificationRef.value?.getCode()
|
||||
uni.showToast({
|
||||
title: res?.data?.message || '发送失败,请稍后重试',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
uni.hideLoading()
|
||||
verificationRef.value?.getCode()
|
||||
uni.showToast({ title: e?.data?.message || '网络异常,发送失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 执行短信登录
|
||||
const doSmsLogin = async () => {
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '正在登录...' })
|
||||
|
||||
try {
|
||||
const res: any = await smsLogin({
|
||||
mobile: smsForm.mobile.trim(),
|
||||
code: smsForm.code.trim(),
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
|
||||
if (res?.data && res.data.success && res.data.result) {
|
||||
const { accessToken, refreshToken } = res.data.result
|
||||
storage.setStoreAccessToken(accessToken)
|
||||
if (refreshToken) {
|
||||
storage.setStoreRefreshToken(refreshToken)
|
||||
}
|
||||
|
||||
try {
|
||||
const [settingsRes, userRes, storeRes] = (await Promise.all([
|
||||
getStoreSettings().catch(() => null),
|
||||
getStoreUserInfo().catch(() => null),
|
||||
getStoreDetail().catch(() => null),
|
||||
])) || []
|
||||
|
||||
const storeInfo = {
|
||||
settings: settingsRes?.data?.result || {},
|
||||
user: userRes?.data?.result || {},
|
||||
store: storeRes?.data?.result || {},
|
||||
}
|
||||
storage.setStoreInfo(storeInfo)
|
||||
} catch (e) {
|
||||
console.warn('获取店铺额外信息失败', e)
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages-sub/merchant/pages/index/index',
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res?.data?.message || '登录失败,请检查验证码',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
uni.showToast({ title: err?.data?.message || '网络连接异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-login-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bg {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||
padding: 100rpx 40rpx 140rpx 40rpx;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.brand-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.badge-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.brand-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: -80rpx 32rpx 40rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 36rpx;
|
||||
box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
margin-bottom: 36rpx;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-bottom: 24rpx;
|
||||
font-size: 30rpx;
|
||||
color: #8c8c8c;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.active-indicator {
|
||||
position: absolute;
|
||||
bottom: -1rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
background: #111827;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
.input-item {
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
.send-btn {
|
||||
padding: 10rpx 20rpx;
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
background: #d1d5db;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: #111827;
|
||||
border-radius: 18rpx;
|
||||
height: 92rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(17, 24, 39, 0.2);
|
||||
|
||||
.btn-text {
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 28rpx;
|
||||
gap: 8rpx;
|
||||
|
||||
.notice-text {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
586
pages-sub/merchant/pages/order/detail.vue
Normal file
586
pages-sub/merchant/pages/order/detail.vue
Normal file
@@ -0,0 +1,586 @@
|
||||
<template>
|
||||
<view class="merchant-order-detail">
|
||||
<!-- 状态头部 -->
|
||||
<view class="status-banner" :class="getStatusClass(orderDetail?.order?.orderStatus)">
|
||||
<view class="status-icon">
|
||||
<u-icon :name="getStatusIcon(orderDetail?.order?.orderStatus)" size="28" color="#ffffff"></u-icon>
|
||||
</view>
|
||||
<view class="status-text-box">
|
||||
<text class="status-title">{{ getStatusText(orderDetail?.order?.orderStatus) }}</text>
|
||||
<view class="sn-row" @click="copySn(orderSn)">
|
||||
<text class="status-sub">订单编号:{{ orderSn }}</text>
|
||||
<u-icon name="file-text" size="13" color="rgba(255,255,255,0.8)"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核销码专属卡片 -->
|
||||
<view class="verify-code-card" v-if="orderDetail?.order?.verificationCode || isAppointment">
|
||||
<view class="card-title-row">
|
||||
<text class="card-title">核销凭证</text>
|
||||
<view class="code-badge">
|
||||
{{ isAppointment ? '预约核销' : '自提核销' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="code-box">
|
||||
<text class="code-number">{{ orderDetail?.order?.verificationCode || '--' }}</text>
|
||||
<view
|
||||
class="verify-now-btn"
|
||||
v-if="orderDetail?.order?.orderStatus === 'TAKE'"
|
||||
@click="handleDirectVerify"
|
||||
>
|
||||
立即核销
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 买家 / 收货信息 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title-row">
|
||||
<text class="card-title">买家信息</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">收货人/自提人:</text>
|
||||
<text class="info-value">{{ orderDetail?.order?.consigneeName || '匿名买家' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系电话:</text>
|
||||
<text class="info-value">{{ maskPhone(orderDetail?.order?.consigneeMobile) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="orderDetail?.order?.consigneeAddressPath">
|
||||
<text class="info-label">地址:</text>
|
||||
<text class="info-value">{{ orderDetail?.order?.consigneeAddressPath }} {{ orderDetail?.order?.consigneeDetail || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品清单(干净规范,拒绝裸露 JSON) -->
|
||||
<view class="info-card">
|
||||
<view class="card-title-row">
|
||||
<text class="card-title">商品清单</text>
|
||||
</view>
|
||||
<view
|
||||
class="goods-row"
|
||||
v-for="(item, idx) in (orderDetail?.orderItems || [])"
|
||||
:key="item.id || item.goodsId || idx"
|
||||
>
|
||||
<image class="goods-img" :src="getGoodsImage(item)" mode="aspectFill"></image>
|
||||
<view class="goods-detail">
|
||||
<text class="goods-title">{{ getGoodsTitle(item) }}</text>
|
||||
<view class="sku-tag" v-if="getCleanSpecs(item)">
|
||||
<text>{{ getCleanSpecs(item) }}</text>
|
||||
</view>
|
||||
<view class="goods-bottom">
|
||||
<text class="price">¥{{ item.unitPrice || item.goodsPrice || '0.00' }}</text>
|
||||
<text class="qty">x{{ item.num || 1 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单财务明细 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title-row">
|
||||
<text class="card-title">费用明细</text>
|
||||
</view>
|
||||
<view class="fee-row">
|
||||
<text class="fee-label">商品总价</text>
|
||||
<text class="fee-val">¥{{ orderDetail?.order?.goodsPrice || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="fee-row" v-if="orderDetail?.order?.discountPrice">
|
||||
<text class="fee-label">优惠金额</text>
|
||||
<text class="fee-val discount">-¥{{ orderDetail?.order?.discountPrice }}</text>
|
||||
</view>
|
||||
<view class="fee-row" v-if="orderDetail?.order?.freightPrice">
|
||||
<text class="fee-label">运费</text>
|
||||
<text class="fee-val">+¥{{ orderDetail?.order?.freightPrice }}</text>
|
||||
</view>
|
||||
<view class="fee-divider"></view>
|
||||
<view class="fee-row total">
|
||||
<text class="fee-label">实收金额</text>
|
||||
<text class="fee-val total-price">¥{{ orderDetail?.order?.flowPrice || '0.00' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单参数(支付方式中文人性化映射) -->
|
||||
<view class="info-card">
|
||||
<view class="info-row">
|
||||
<text class="info-label">下单时间:</text>
|
||||
<text class="info-value">{{ orderDetail?.order?.createTime || '--' }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="orderDetail?.order?.paymentTime">
|
||||
<text class="info-label">支付时间:</text>
|
||||
<text class="info-value">{{ orderDetail?.order?.paymentTime }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="orderDetail?.order?.paymentMethod">
|
||||
<text class="info-label">支付方式:</text>
|
||||
<text class="info-value pay-method-val">{{ formatPaymentMethod(orderDetail?.order?.paymentMethod) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getStoreOrderDetail, storeOrderTake } from '../../api/order.js'
|
||||
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||
|
||||
const orderSn = ref('')
|
||||
const orderDetail = ref<any>(null)
|
||||
|
||||
const isAppointment = computed(() => {
|
||||
return orderDetail.value?.order?.orderType === 'APPOINTMENT_GOODS'
|
||||
})
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options?.sn) {
|
||||
orderSn.value = options.sn
|
||||
fetchDetail()
|
||||
}
|
||||
})
|
||||
|
||||
const fetchDetail = async () => {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
try {
|
||||
const res: any = await getStoreOrderDetail(orderSn.value)
|
||||
uni.hideLoading()
|
||||
if (res?.data && res.data.success && res.data.result) {
|
||||
orderDetail.value = res.data.result
|
||||
} else {
|
||||
uni.showToast({ title: '获取订单详情失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 复制单号
|
||||
const copySn = (sn: string) => {
|
||||
if (!sn) return
|
||||
uni.setClipboardData({
|
||||
data: sn,
|
||||
success: () => {
|
||||
uni.showToast({ title: '单号已复制', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 提取商品标题
|
||||
const getGoodsTitle = (item: any) => {
|
||||
if (item.goodsName && item.goodsName.trim()) return item.goodsName
|
||||
if (item.name && item.name.trim()) return item.name
|
||||
if (orderDetail.value?.order?.orderName) return orderDetail.value.order.orderName
|
||||
return '精选商品'
|
||||
}
|
||||
|
||||
// 智能提取商品图片,过滤并兼顾富文本/JSON 结构
|
||||
const getGoodsImage = (item: any) => {
|
||||
if (item.image) {
|
||||
const parsed = parseGoodsImageUrl(item.image)
|
||||
if (parsed) return parsed
|
||||
}
|
||||
// 若 image 为空,尝试从 specs JSON 里抓取合法的图片 url
|
||||
if (item.specs && typeof item.specs === 'string') {
|
||||
const match = item.specs.match(/https?:\/\/[^\s"'\\}]+\.(jpeg|jpg|png|webp|gif)/i)
|
||||
if (match) return match[0]
|
||||
}
|
||||
return '/static/tabbar/home.png'
|
||||
}
|
||||
|
||||
// 智能提炼清洁规格,绝不裸露冗长的原始 JSON
|
||||
const getCleanSpecs = (item: any) => {
|
||||
const specs = item.specs
|
||||
if (!specs || !String(specs).trim()) return ''
|
||||
const trimmed = String(specs).trim()
|
||||
|
||||
// 若为 JSON 格式
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
const obj = JSON.parse(trimmed)
|
||||
const ignoreKeys = ['images', 'code', 'message', 'result', 'success', 'showProgress', 'url', 'status']
|
||||
const pairs: string[] = []
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (!ignoreKeys.includes(k) && typeof v === 'string' && !v.startsWith('http') && v.length < 50) {
|
||||
pairs.push(`${k}: ${v}`)
|
||||
}
|
||||
}
|
||||
if (pairs.length > 0) return pairs.join(' / ')
|
||||
} catch (e) {}
|
||||
|
||||
// 解析失败正则提取业务键值对
|
||||
const matches = [...trimmed.matchAll(/"([^"]{1,20})"\s*:\s*"([^"]{1,30})"/g)]
|
||||
const ignoreKeys = ['code', 'message', 'success', 'url', 'result', 'showProgress', 'status', 'name']
|
||||
const pairs: string[] = []
|
||||
for (const m of matches) {
|
||||
const k = m[1]
|
||||
const v = m[2]
|
||||
if (!ignoreKeys.includes(k) && !v.startsWith('http')) {
|
||||
pairs.push(`${k}: ${v}`)
|
||||
}
|
||||
}
|
||||
if (pairs.length > 0) return pairs.join(' / ')
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 纯文本规格
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// 支付方式中文映射
|
||||
const formatPaymentMethod = (method: string) => {
|
||||
if (!method) return '--'
|
||||
const map: Record<string, string> = {
|
||||
WALLET: '余额支付',
|
||||
ALIPAY: '支付宝支付',
|
||||
ALIPAY_PC: '支付宝支付',
|
||||
ALIPAY_WAP: '支付宝支付',
|
||||
ALIPAY_APP: '支付宝支付',
|
||||
WECHAT: '微信支付',
|
||||
WEIXIN: '微信支付',
|
||||
WECHAT_MP: '微信支付',
|
||||
WEIXIN_MP: '微信支付',
|
||||
WX_JSAPI: '微信支付',
|
||||
WX_APP: '微信支付',
|
||||
WX_NATIVE: '微信支付',
|
||||
WX_H5: '微信支付',
|
||||
BANK_TRANSFER: '银行转账',
|
||||
POINT: '积分兑换',
|
||||
CASH: '现金支付',
|
||||
OFFLINE: '线下支付',
|
||||
FREE: '免费免付',
|
||||
}
|
||||
const key = String(method).toUpperCase().trim()
|
||||
return map[key] || method
|
||||
}
|
||||
|
||||
const maskPhone = (phone: string) => {
|
||||
if (!phone || phone.length < 7) return phone || ''
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
UNPAID: '待付款',
|
||||
PAID: '已付款',
|
||||
UNDELIVERED: '待发货',
|
||||
DELIVERED: '待收货',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
TAKE: '待核销',
|
||||
}
|
||||
return map[status] || status || '处理中'
|
||||
}
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
if (status === 'TAKE') return 'bg-take'
|
||||
if (status === 'COMPLETED') return 'bg-done'
|
||||
if (status === 'CANCELLED') return 'bg-cancel'
|
||||
return 'bg-normal'
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
if (status === 'TAKE') return 'checkmark-circle-fill'
|
||||
if (status === 'COMPLETED') return 'checkmark'
|
||||
if (status === 'CANCELLED') return 'close'
|
||||
return 'clock'
|
||||
}
|
||||
|
||||
// 直接在详情页核销
|
||||
const handleDirectVerify = async () => {
|
||||
const code = orderDetail.value?.order?.verificationCode
|
||||
if (!code) {
|
||||
uni.showToast({ title: '该订单暂无可核销码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认核销',
|
||||
content: `确认核销此单?核销码:${code}`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '正在核销...' })
|
||||
try {
|
||||
const takeRes: any = await storeOrderTake(orderSn.value, code)
|
||||
uni.hideLoading()
|
||||
if (takeRes?.data && takeRes.data.success) {
|
||||
uni.showToast({ title: '核销成功', icon: 'success' })
|
||||
fetchDetail()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: takeRes?.data?.message || '核销失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '核销异常,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-order-detail {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f5f7;
|
||||
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 44rpx 36rpx;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.bg-take {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
&.bg-normal {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||
}
|
||||
&.bg-done {
|
||||
background: #6b7280;
|
||||
}
|
||||
&.bg-cancel {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 34rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.status-text-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.status-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sn-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
|
||||
.status-sub {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verify-code-card {
|
||||
margin: -24rpx 28rpx 24rpx 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.code-badge {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.code-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
|
||||
.code-number {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.verify-now-btn {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
padding: 14rpx 32rpx;
|
||||
border-radius: 14rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(17, 24, 39, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: 0 28rpx 24rpx 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
||||
|
||||
.card-title-row {
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 14rpx;
|
||||
border-bottom: 1rpx solid #f9fafb;
|
||||
|
||||
.card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #6b7280;
|
||||
width: 180rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #111827;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
|
||||
&.pay-method-val {
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-row {
|
||||
display: flex;
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid #f9fafb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #f3f4f6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-detail {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.goods-title {
|
||||
font-size: 28rpx;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sku-tag {
|
||||
margin-top: 6rpx;
|
||||
|
||||
text {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-top: 10rpx;
|
||||
|
||||
.price {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.qty {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fee-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 14rpx;
|
||||
color: #6b7280;
|
||||
|
||||
.discount {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.total {
|
||||
margin-top: 16rpx;
|
||||
margin-bottom: 0;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
|
||||
.total-price {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fee-divider {
|
||||
height: 1rpx;
|
||||
background: #f3f4f6;
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
653
pages-sub/merchant/pages/order/list.vue
Normal file
653
pages-sub/merchant/pages/order/list.vue
Normal file
@@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<view class="merchant-order-page">
|
||||
<!-- 顶部吸顶搜索栏与 Tab -->
|
||||
<view class="sticky-header">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-box">
|
||||
<view class="search-input-wrap">
|
||||
<u-icon name="search" size="18" color="#9ca3af"></u-icon>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="keywords"
|
||||
placeholder="搜索订单号 / 买家手机号"
|
||||
placeholder-class="placeholder"
|
||||
confirm-type="search"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
<view class="clear-btn" v-if="keywords" @click="clearSearch">
|
||||
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-action-btn" v-if="keywords" @click="onSearch">
|
||||
<text>搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态横向滑动 Tab 栏(绝不折行) -->
|
||||
<scroll-view class="tab-scroll" scroll-x :show-scrollbar="false" :scroll-with-animation="true">
|
||||
<view class="tab-inner">
|
||||
<view
|
||||
class="tab-item"
|
||||
v-for="item in tabList"
|
||||
:key="item.key"
|
||||
:class="{ active: currentTab === item.key }"
|
||||
@click="switchTab(item.key)"
|
||||
>
|
||||
<text class="tab-label">{{ item.label }}</text>
|
||||
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表主体 -->
|
||||
<view class="order-list-body">
|
||||
<!-- 列表数据 -->
|
||||
<view
|
||||
class="order-card"
|
||||
v-for="order in orderList"
|
||||
:key="order.sn"
|
||||
@click="goToDetail(order.sn)"
|
||||
>
|
||||
<!-- 头部:单号与状态 -->
|
||||
<view class="card-head">
|
||||
<view class="sn-box" @click.stop="copySn(order.sn)">
|
||||
<text class="order-sn">单号:{{ order.sn }}</text>
|
||||
<u-icon name="file-text" size="14" color="#9ca3af"></u-icon>
|
||||
</view>
|
||||
<view class="status-badge" :class="getStatusClass(order.orderStatus)">
|
||||
{{ getStatusText(order.orderStatus) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品信息 -->
|
||||
<view class="goods-container">
|
||||
<view
|
||||
class="goods-item"
|
||||
v-for="goods in (order.orderItems || [])"
|
||||
:key="goods.id || goods.goodsId"
|
||||
>
|
||||
<image class="goods-thumb" :src="goods.image || '/static/tabbar/home.png'" mode="aspectFill"></image>
|
||||
<view class="goods-info">
|
||||
<text class="goods-name">{{ getGoodsName(goods, order) }}</text>
|
||||
<view class="spec-pill" v-if="getSpecText(goods)">
|
||||
<text>{{ getSpecText(goods) }}</text>
|
||||
</view>
|
||||
<view class="price-qty-row">
|
||||
<text class="goods-price">¥{{ goods.unitPrice || goods.goodsPrice || '0.00' }}</text>
|
||||
<text class="goods-qty">x{{ goods.num || 1 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单金额与买家信息 -->
|
||||
<view class="card-foot">
|
||||
<view class="buyer-info" v-if="order.consigneeMobile || order.memberPhone">
|
||||
<u-icon name="account" size="14" color="#9ca3af"></u-icon>
|
||||
<text class="buyer-tel">{{ maskPhone(order.consigneeMobile || order.memberPhone) }}</text>
|
||||
</view>
|
||||
<view class="buyer-info" v-else>
|
||||
<text class="order-type-tag" v-if="order.orderType === 'APPOINTMENT_GOODS'">预约商品单</text>
|
||||
<text class="order-type-tag" v-else-if="order.verificationCode">自提单</text>
|
||||
</view>
|
||||
|
||||
<view class="total-bar">
|
||||
<text class="total-label">实付:</text>
|
||||
<text class="total-symbol">¥</text>
|
||||
<text class="total-price">{{ order.flowPrice || '0.00' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核销专属快捷动作 -->
|
||||
<view class="card-actions" v-if="order.verificationCode || order.orderType === 'APPOINTMENT_GOODS' || order.orderStatus === 'TAKE'">
|
||||
<view class="action-btn verify" @click.stop="quickVerify(order)">
|
||||
<u-icon name="scan" size="15" color="#ffffff"></u-icon>
|
||||
<text class="btn-lbl">扫码/输码核销</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && orderList.length === 0">
|
||||
<u-icon name="order" size="64" color="#d1d5db"></u-icon>
|
||||
<text class="empty-text">暂无相关订单记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="loading-more" v-if="loading">
|
||||
<text>正在加载订单数据...</text>
|
||||
</view>
|
||||
<view class="no-more" v-else-if="finished && orderList.length > 0">
|
||||
<text>— 没有更多了 —</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getStoreOrderList } from '../../api/order.js'
|
||||
|
||||
const keywords = ref('')
|
||||
const currentTab = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const orderList = ref<any[]>([])
|
||||
|
||||
// 优化 Tab 标题,保持精练统一、绝不换行
|
||||
const tabList = [
|
||||
{ key: 'ALL', label: '全部' },
|
||||
{ key: 'TAKE', label: '待核销' },
|
||||
{ key: 'UNDELIVERED', label: '待发货' },
|
||||
{ key: 'DELIVERED', label: '待收货' },
|
||||
{ key: 'COMPLETED', label: '已完成' },
|
||||
]
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options?.tab) {
|
||||
currentTab.value = options.tab
|
||||
}
|
||||
fetchOrders(true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchOrders(true)
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!finished.value && !loading.value) {
|
||||
pageNumber.value++
|
||||
fetchOrders(false)
|
||||
}
|
||||
})
|
||||
|
||||
const fetchOrders = async (reset = false) => {
|
||||
if (reset) {
|
||||
pageNumber.value = 1
|
||||
orderList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
const params: any = {
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: pageSize.value,
|
||||
keywords: keywords.value.trim() || undefined,
|
||||
}
|
||||
|
||||
if (currentTab.value !== 'ALL') {
|
||||
params.orderStatus = currentTab.value
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await getStoreOrderList(params)
|
||||
loading.value = false
|
||||
|
||||
if (res?.data && res.data.success) {
|
||||
const records = res.data.result?.records || []
|
||||
if (reset) {
|
||||
orderList.value = records
|
||||
} else {
|
||||
orderList.value = [...orderList.value, ...records]
|
||||
}
|
||||
|
||||
if (records.length < pageSize.value) {
|
||||
finished.value = true
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res?.data?.message || '获取订单列表失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
loading.value = false
|
||||
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (key: string) => {
|
||||
if (currentTab.value === key) return
|
||||
currentTab.value = key
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
keywords.value = ''
|
||||
fetchOrders(true)
|
||||
}
|
||||
|
||||
const goToDetail = (sn: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages-sub/merchant/pages/order/detail?sn=${sn}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 快捷核销
|
||||
const quickVerify = (order: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages-sub/merchant/pages/order/detail?sn=${order.sn}&autoVerify=1`,
|
||||
})
|
||||
}
|
||||
|
||||
// 复制单号
|
||||
const copySn = (sn: string) => {
|
||||
if (!sn) return
|
||||
uni.setClipboardData({
|
||||
data: sn,
|
||||
success: () => {
|
||||
uni.showToast({ title: '单号已复制', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 提取商品名(容错兜底)
|
||||
const getGoodsName = (goods: any, order: any) => {
|
||||
if (goods.goodsName && goods.goodsName.trim()) return goods.goodsName
|
||||
if (goods.name && goods.name.trim()) return goods.name
|
||||
if (order.orderName && order.orderName.trim()) return order.orderName
|
||||
if (goods.specs && goods.specs.trim()) return goods.specs
|
||||
return '精选商品'
|
||||
}
|
||||
|
||||
// 提取规格文本(智能剔除多余字段)
|
||||
const getSpecText = (goods: any) => {
|
||||
const specs = goods.specs
|
||||
if (!specs || !String(specs).trim()) return ''
|
||||
if (specs === goods.goodsName) return ''
|
||||
|
||||
// 处理 JSON 字符串形式
|
||||
if (typeof specs === 'string' && (specs.startsWith('{') || specs.startsWith('['))) {
|
||||
try {
|
||||
const obj = JSON.parse(specs)
|
||||
if (typeof obj === 'object') {
|
||||
const values = Object.values(obj).filter(Boolean)
|
||||
if (values.length) return values.join(' / ')
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return String(specs)
|
||||
}
|
||||
|
||||
// 手机号脱敏
|
||||
const maskPhone = (phone: string) => {
|
||||
if (!phone) return ''
|
||||
const str = String(phone).trim()
|
||||
if (str.length === 11) {
|
||||
return str.substring(0, 3) + '****' + str.substring(7)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 状态文字映射
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
UNPAID: '待付款',
|
||||
PAID: '已付款',
|
||||
UNDELIVERED: '待发货',
|
||||
DELIVERED: '待收货',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
TAKE: '待核销',
|
||||
}
|
||||
return map[status] || status || '处理中'
|
||||
}
|
||||
|
||||
// 状态样式类
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'TAKE':
|
||||
return 'status-take'
|
||||
case 'COMPLETED':
|
||||
return 'status-done'
|
||||
case 'CANCELLED':
|
||||
return 'status-cancel'
|
||||
default:
|
||||
return 'status-normal'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-order-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f4f5f7;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.search-box {
|
||||
padding: 16rpx 28rpx 10rpx 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.search-input-wrap {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: #f3f4f6;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-left: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #9ca3af;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.search-action-btn {
|
||||
font-size: 28rpx;
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
|
||||
.tab-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16rpx;
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
min-width: 130rpx;
|
||||
text-align: center;
|
||||
padding: 22rpx 12rpx;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
.tab-label {
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-label {
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 44rpx;
|
||||
height: 6rpx;
|
||||
background: #111827;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-list-body {
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
.order-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f3f4f6;
|
||||
|
||||
.sn-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.order-sn {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
|
||||
&.status-take {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
&.status-done {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
&.status-cancel {
|
||||
background: #f9fafb;
|
||||
color: #9ca3af;
|
||||
}
|
||||
&.status-normal {
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-container {
|
||||
padding: 10rpx 0;
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #fafafa;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.goods-thumb {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #f9fafb;
|
||||
border: 1rpx solid #f3f4f6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.goods-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spec-pill {
|
||||
margin-top: 6rpx;
|
||||
|
||||
text {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.price-qty-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-top: 10rpx;
|
||||
|
||||
.goods-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.goods-qty {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f9fafb;
|
||||
|
||||
.buyer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.buyer-tel {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-type-tag {
|
||||
font-size: 20rpx;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.total-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.total-label {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.total-symbol {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
border-top: 1rpx dashed #e5e7eb;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 36rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.verify {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(17, 24, 39, 0.15);
|
||||
|
||||
.btn-lbl {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
155
pages-sub/merchant/utils/storeRequest.js
Normal file
155
pages-sub/merchant/utils/storeRequest.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import api from "@/config/api.js";
|
||||
import Request from "@/lib/request/index.js";
|
||||
import Foundation from "@/utils/Foundation.js";
|
||||
import { md5 } from "@/utils/md5.js";
|
||||
import storage from "@/utils/storage.js";
|
||||
import uuid from "@/utils/uuid.modified.js";
|
||||
|
||||
let isStoreNavigateTo = false;
|
||||
|
||||
function cleanStoreAuth() {
|
||||
uni.showToast({
|
||||
title: "商家登录状态已过期,请重新登录",
|
||||
icon: "none",
|
||||
duration: 1800,
|
||||
});
|
||||
|
||||
storage.clearStoreAuth();
|
||||
|
||||
if (!isStoreNavigateTo) {
|
||||
isStoreNavigateTo = true;
|
||||
setTimeout(() => {
|
||||
isStoreNavigateTo = false;
|
||||
uni.reLaunch({
|
||||
url: "/pages-sub/merchant/pages/login/index",
|
||||
});
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
const storeHttp = new Request();
|
||||
|
||||
const ensureUuid = () => {
|
||||
if (!storage.getUuid()) {
|
||||
storage.setUuid(uuid.v1());
|
||||
}
|
||||
};
|
||||
|
||||
storeHttp.setConfig((config) => {
|
||||
ensureUuid();
|
||||
config.baseURL = api.store;
|
||||
config.header = {
|
||||
...config.header,
|
||||
};
|
||||
config.validateStatus = () => true;
|
||||
return config;
|
||||
});
|
||||
|
||||
storeHttp.interceptors.request.use(
|
||||
(config) => {
|
||||
ensureUuid();
|
||||
const token = storage.getStoreAccessToken();
|
||||
if (token) {
|
||||
const nonce = Foundation.randomString(6);
|
||||
const timestamp = parseInt(new Date().getTime() / 1000);
|
||||
const sign = md5(nonce + timestamp + token);
|
||||
const _params = { nonce, timestamp, sign };
|
||||
config.params = { ...(config.params || {}), ..._params };
|
||||
config.header.accessToken = token;
|
||||
}
|
||||
config.header = {
|
||||
...config.header,
|
||||
uuid: storage.getUuid(),
|
||||
};
|
||||
return config;
|
||||
},
|
||||
(config) => Promise.reject(config)
|
||||
);
|
||||
|
||||
let isStoreRefreshing = false;
|
||||
let storeRequests = [];
|
||||
|
||||
function refreshTokenApi(refreshToken) {
|
||||
return uni.request({
|
||||
url: `${api.store}/passport/login/refresh/${refreshToken}`,
|
||||
method: "GET",
|
||||
header: {
|
||||
uuid: storage.getUuid(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
storeHttp.interceptors.response.use(
|
||||
async (response) => {
|
||||
isStoreNavigateTo = false;
|
||||
try {
|
||||
uni.hideLoading();
|
||||
} catch (e) {}
|
||||
|
||||
const token = storage.getStoreAccessToken();
|
||||
const refreshToken = storage.getStoreRefreshToken();
|
||||
|
||||
// 403 / 401 并且有 refreshToken,尝试刷新
|
||||
if (
|
||||
token &&
|
||||
refreshToken &&
|
||||
(response.statusCode === 403 || response.statusCode === 401 || response.data?.code === 403)
|
||||
) {
|
||||
if (!isStoreRefreshing) {
|
||||
isStoreRefreshing = true;
|
||||
storage.removeStoreAccessToken();
|
||||
try {
|
||||
const [err, res] = await refreshTokenApi(refreshToken);
|
||||
const data = res?.data || err?.data;
|
||||
if (data && data.success && data.result) {
|
||||
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = data.result;
|
||||
storage.setStoreAccessToken(newAccessToken);
|
||||
if (newRefreshToken) {
|
||||
storage.setStoreRefreshToken(newRefreshToken);
|
||||
}
|
||||
response.header = response.header || {};
|
||||
response.header.accessToken = `${newAccessToken}`;
|
||||
storeRequests.forEach((cb) => cb(newAccessToken));
|
||||
storeRequests = [];
|
||||
return storeHttp.request(response.config);
|
||||
} else {
|
||||
cleanStoreAuth();
|
||||
return Promise.reject(data);
|
||||
}
|
||||
} catch (e) {
|
||||
cleanStoreAuth();
|
||||
return Promise.reject(e);
|
||||
} finally {
|
||||
isStoreRefreshing = false;
|
||||
}
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
storeRequests.push((newToken) => {
|
||||
response.header = response.header || {};
|
||||
response.header.accessToken = `${newToken}`;
|
||||
resolve(storeHttp.request(response.config));
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
(!token && (response.statusCode === 403 || response.statusCode === 401)) ||
|
||||
response.data?.code === 403 ||
|
||||
response.data?.code === 401
|
||||
) {
|
||||
cleanStoreAuth();
|
||||
} else if ((response.statusCode === 200 && response.data && !response.data.success) || response.statusCode === 400) {
|
||||
if (response.data?.message) {
|
||||
uni.showToast({
|
||||
title: response.data.message,
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => error
|
||||
);
|
||||
|
||||
export { storeHttp };
|
||||
export default storeHttp;
|
||||
1963
pages.json
1963
pages.json
File diff suppressed because it is too large
Load Diff
@@ -1,302 +1,262 @@
|
||||
<template>
|
||||
<view class="coupon-center">
|
||||
<div class="swiper-box">
|
||||
<div class="swiper-item">
|
||||
<div class="scroll-v" enableBackToTop="true" scroll-y>
|
||||
<u-empty mode="coupon" style='margin-top: 20%;' text="没有优惠券了" v-if="whetherEmpty"></u-empty>
|
||||
<view v-else class="coupon-item" v-for="(item, index) in couponList" :key="index">
|
||||
<view class="left">
|
||||
<view class="wave-line">
|
||||
<view class="wave" v-for="(item, index) in 12" :key="index"></view>
|
||||
</view>
|
||||
<view class="message">
|
||||
<view>
|
||||
<!--判断当前优惠券类型 couponType PRICE || DISCOUNT -->
|
||||
<span v-if="item.couponType == 'DISCOUNT'">{{ item.couponDiscount }}折</span>
|
||||
<span v-else>{{ item.price }}元</span>
|
||||
</view>
|
||||
<view>满{{ item.consumeThreshold | unitPrice }}元可用</view>
|
||||
</view>
|
||||
<view class="circle circle-top"></view>
|
||||
<view class="circle circle-bottom"></view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<view>
|
||||
<!-- 根据scopeType 判断是否是 平台、品类或店铺 -->
|
||||
<view class="coupon-title wes-3" v-if="item.scopeType">
|
||||
<span v-if="item.scopeType == 'ALL' && item.storeId == '0'">全平台</span>
|
||||
<span v-if="item.scopeType == 'PORTION_GOODS_CATEGORY'">仅限品类</span>
|
||||
<view v-else>{{ item.storeName == 'platform' ? '全平台' :item.storeName+'店铺' }}使用
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.endTime">有效期至:{{ item.endTime.split(" ")[0] }}</view>
|
||||
</view>
|
||||
<view class="receive" @click="receive(item)">
|
||||
<text>点击</text><br />
|
||||
<text>领取</text>
|
||||
</view>
|
||||
<view class="bg-quan"> 券 </view>
|
||||
</view>
|
||||
<view class="coupon-list">
|
||||
<u-empty mode="coupon" style='margin-top: 20%;' text="没有优惠券了" v-if="whetherEmpty"></u-empty>
|
||||
<view v-else class="coupon-card" v-for="(item, index) in couponList" :key="index">
|
||||
<view class="coupon-card-left">
|
||||
<text class="coupon-price-symbol">¥</text>
|
||||
<text class="coupon-price-value">{{ item.couponType == 'DISCOUNT' ? item.couponDiscount + '折' : unitPrice(item.price) }}</text>
|
||||
</view>
|
||||
<view class="coupon-divider"></view>
|
||||
<view class="coupon-card-right">
|
||||
<view class="coupon-info">
|
||||
<text class="coupon-card-name">{{ item.storeName == 'platform' ? '全平台' : item.storeName + '店铺' }}使用</text>
|
||||
<text class="coupon-card-desc">满{{unitPrice(item.consumeThreshold) }}元可用</text>
|
||||
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime.split(" ")[0] }}</text>
|
||||
</view>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="coupon-claim-btn" @click="receive(item)">
|
||||
领取
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
receiveCoupons
|
||||
} from "@/api/members.js";
|
||||
import {
|
||||
getAllCoupons
|
||||
} from "@/api/promotions.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loadStatus: "more", //下拉状态
|
||||
whetherEmpty: false, //是否为空
|
||||
couponList: [], // 优惠券列表
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
storeId: "", //店铺 id,
|
||||
couponData: ""
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
this.storeId = option.storeId;
|
||||
this.getCoupon();
|
||||
},
|
||||
onReachBottom() {
|
||||
<script setup lang="ts">
|
||||
import { receiveCoupons } from '@/api/members.js'
|
||||
import { getAllCoupons } from '@/api/promotions.js'
|
||||
import { useStore } from '@/store'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import {
|
||||
onLoad,
|
||||
onNavigationBarButtonTap,
|
||||
onPullDownRefresh,
|
||||
onReachBottom,
|
||||
} from '@dcloudio/uni-app'
|
||||
import { getCurrentInstance, ref } from 'vue'
|
||||
|
||||
this.loadMore()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
//下拉刷新
|
||||
this.params.pageNumber = 1;
|
||||
this.couponList = [];
|
||||
this.getCoupon();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取当前优惠券
|
||||
*/
|
||||
getCoupon() {
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
});
|
||||
let submitData = {
|
||||
...this.params
|
||||
};
|
||||
// 判断当前是否有店铺
|
||||
this.storeId ? (submitData = {
|
||||
...this.params,
|
||||
storeId: this.storeId
|
||||
}) : "",
|
||||
getAllCoupons(submitData)
|
||||
.then((res) => {
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
uni.stopPullDownRefresh();
|
||||
if (res.data.code == 200) {
|
||||
// 如果请求成功,展示数据并进行展示
|
||||
this.couponData = res.data.result
|
||||
if (this.couponData.total == 0) {
|
||||
// 当本次请求数据为空展示空信息
|
||||
this.whetherEmpty = true;
|
||||
} else {
|
||||
this.couponList.push(...this.couponData.records);
|
||||
this.loadStatus = "noMore";
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 领取优惠券
|
||||
*/
|
||||
receive(val) {
|
||||
this.$u.throttle(()=>{
|
||||
this.fetchCoupon(val)
|
||||
}, 1500)
|
||||
|
||||
},
|
||||
const store = useStore()
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
fetchCoupon(val){
|
||||
receiveCoupons(val.id).then((res) => {
|
||||
if (res.data.code == 200) {
|
||||
uni.showToast({
|
||||
title: "领取成功",
|
||||
icon: "none",
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
const loadStatus = ref('more')
|
||||
const whetherEmpty = ref(false)
|
||||
const couponList = ref<any[]>([])
|
||||
const params = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const storeId = ref('')
|
||||
const couponData = ref<any>(null)
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
loadMore() {
|
||||
if (this.couponData.total > this.params.pageNumber * this.params.pageSize) {
|
||||
this.params.pageNumber++;
|
||||
this.getCoupon();
|
||||
}
|
||||
},
|
||||
},
|
||||
onNavigationBarButtonTap(e) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/cart/coupon/couponIntro",
|
||||
});
|
||||
},
|
||||
};
|
||||
onLoad((option) => {
|
||||
storeId.value = option.storeId || ''
|
||||
getCoupon()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
params.value.pageNumber = 1
|
||||
couponList.value = []
|
||||
getCoupon()
|
||||
})
|
||||
|
||||
onNavigationBarButtonTap(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/cart/coupon/couponIntro',
|
||||
})
|
||||
})
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
function getCoupon() {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
const submitData = storeId.value
|
||||
? { ...params.value, storeId: storeId.value }
|
||||
: { ...params.value }
|
||||
|
||||
getAllCoupons(submitData)
|
||||
.then((res) => {
|
||||
hideLoadingIfNeeded()
|
||||
uni.stopPullDownRefresh()
|
||||
if (res.data.code == 200) {
|
||||
couponData.value = res.data.result
|
||||
if (couponData.value.total == 0) {
|
||||
whetherEmpty.value = true
|
||||
} else {
|
||||
couponList.value.push(...couponData.value.records)
|
||||
loadStatus.value = 'noMore'
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function receive(val: any) {
|
||||
proxy.$u.throttle(() => {
|
||||
fetchCoupon(val)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function fetchCoupon(val: any) {
|
||||
receiveCoupons(val.id).then((res) => {
|
||||
if (res.data.code == 200) {
|
||||
uni.showToast({ title: '领取成功', icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message, icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (couponData.value?.total > params.value.pageNumber * params.value.pageSize) {
|
||||
params.value.pageNumber++
|
||||
getCoupon()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.coupon-center {
|
||||
height: 100%;
|
||||
.coupon-center {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 220rpx;
|
||||
margin: 20rpx;
|
||||
.coupon-list {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.left {
|
||||
height: 100%;
|
||||
width: 260rpx;
|
||||
background-color: $light-color;
|
||||
position: relative;
|
||||
.coupon-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
border: 1rpx solid #f0f1f5;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
.message {
|
||||
color: $font-color-white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 40rpx;
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
view:nth-child(1) {
|
||||
font-weight: bold;
|
||||
font-size: 60rpx;
|
||||
}
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 50%;
|
||||
left: 204rpx;
|
||||
z-index: 2;
|
||||
box-shadow: inset 0 0 0 1rpx #eceef2;
|
||||
}
|
||||
|
||||
view:nth-child(2) {
|
||||
font-size: $font-sm;
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
top: -16rpx;
|
||||
}
|
||||
|
||||
.wave-line {
|
||||
height: 220rpx;
|
||||
width: 8rpx;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $light-color;
|
||||
overflow: hidden;
|
||||
&::after {
|
||||
bottom: -16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.wave {
|
||||
width: 8rpx;
|
||||
height: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0 16rpx 16rpx 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
.coupon-card-left {
|
||||
width: 220rpx;
|
||||
min-height: 180rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #ff3b30;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffecec 100%);
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: $bg-color;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
z-index: 111;
|
||||
}
|
||||
.coupon-price-symbol {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.circle-top {
|
||||
top: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
.coupon-price-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.circle-bottom {
|
||||
bottom: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
}
|
||||
.coupon-divider {
|
||||
position: absolute;
|
||||
left: 220rpx;
|
||||
top: 24rpx;
|
||||
bottom: 24rpx;
|
||||
width: 0;
|
||||
border-left: 2rpx dashed #eceef2;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 450rpx;
|
||||
font-size: $font-sm;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.coupon-card-right {
|
||||
flex: 1;
|
||||
padding: 32rpx 32rpx 32rpx 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
>view:nth-child(1) {
|
||||
color: #666666;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
.coupon-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
>view:nth-child(1) {
|
||||
color: #ff6262;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
.coupon-card-name {
|
||||
font-size: 30rpx;
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.receive {
|
||||
color: #ffffff;
|
||||
background-color: $main-color;
|
||||
border-radius: 50%;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
text-align: center;
|
||||
margin-right: 30rpx;
|
||||
vertical-align: middle;
|
||||
padding-top: 8rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.coupon-card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8a8f99;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bg-quan {
|
||||
width: 244rpx;
|
||||
height: 244rpx;
|
||||
border: 6rpx solid $main-color;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
color: $main-color;
|
||||
text-align: center;
|
||||
padding-top: 30rpx;
|
||||
font-size: 130rpx;
|
||||
position: absolute;
|
||||
right: -54rpx;
|
||||
bottom: -60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.coupon-card-time {
|
||||
font-size: 22rpx;
|
||||
color: #b0b3bf;
|
||||
}
|
||||
|
||||
.coupon-title {
|
||||
width: 260rpx;
|
||||
.coupon-claim-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff6b35, #ff4b2b);
|
||||
box-shadow: 0 8rpx 16rpx rgba(255, 75, 43, 0.25);
|
||||
|
||||
}
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,51 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<view :style="themeStyle" class="content">
|
||||
<view class="body">
|
||||
<view class="top-view">
|
||||
<view class="title">{{coupon.title}}</view>
|
||||
<view class="price" v-if="coupon.couponType =='PRICE'"><text>¥</text>{{coupon.price | unitPrice}}</view>
|
||||
<view class="price" v-if="coupon.couponType =='DISCOUNT'">{{coupon.discount}}折</view>
|
||||
<view class="text">满{{coupon.consumeThreshold}}元可用</view>
|
||||
<view class="bg-quan">
|
||||
券
|
||||
</view>
|
||||
<view class="jiao-1" :class="{'used-color':coupon.used_status!=0}">
|
||||
<text class="text-1">{{coupon.used_status == 0?'新到':coupon.used_status_text}}</text>
|
||||
<view class="title">{{ coupon.title }}</view>
|
||||
<view class="price" v-if="coupon.couponType == 'PRICE'"><text>¥</text>{{ unitPrice(coupon.price) }}</view>
|
||||
<view class="price" v-if="coupon.couponType == 'DISCOUNT'">{{ coupon.discount }}折</view>
|
||||
<view class="text">满{{ coupon.consumeThreshold }}元可用</view>
|
||||
<view class="bg-quan">券</view>
|
||||
<view class="jiao-1" :class="{ 'used-color': coupon.used_status != 0 }">
|
||||
<text class="text-1">{{ coupon.used_status == 0 ? '新到' : coupon.used_status_text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-view">
|
||||
<view class="text">• 使用范围:{{
|
||||
coupon.scopeType == 'ALL' && coupon.storeId == '0'
|
||||
? "全平台"
|
||||
: coupon.scopeType == "PORTION_GOODS"
|
||||
? "部分商品"
|
||||
: coupon.scopeType == "PORTION_GOODS_CATEGORY"
|
||||
? "部分分类商品"
|
||||
: coupon.storeName == 'platform' ? '全平台' :coupon.storeName+''
|
||||
}}使用</view>
|
||||
<view class="text">• 有效期至:{{coupon.endTime}}</view>
|
||||
|
||||
coupon.scopeType == 'ALL' && coupon.storeId == '0'
|
||||
? '全平台'
|
||||
: coupon.scopeType == 'PORTION_GOODS'
|
||||
? '部分商品'
|
||||
: coupon.scopeType == 'PORTION_GOODS_CATEGORY'
|
||||
? '部分分类商品'
|
||||
: coupon.storeName == 'platform' ? '全平台' : coupon.storeName + ''
|
||||
}}使用</view>
|
||||
<view class="text">• 有效期至:{{ coupon.endTime }}</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
coupon: {}, //优惠券数据
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
this.coupon = JSON.parse(decodeURIComponent(option.item));
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const coupon = ref<Record<string, any>>({})
|
||||
|
||||
onLoad((option) => {
|
||||
coupon.value = JSON.parse(decodeURIComponent(option.item))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page,
|
||||
.content {
|
||||
// background: $main-color;
|
||||
height: 100%;
|
||||
}
|
||||
.body {
|
||||
|
||||
@@ -1,292 +1,323 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<u-tabs
|
||||
:list="list"
|
||||
:is-scroll="false"
|
||||
:active-color="lightColor"
|
||||
:current="current"
|
||||
@change="
|
||||
(i) => {
|
||||
current = i;
|
||||
}
|
||||
"
|
||||
>
|
||||
</u-tabs>
|
||||
|
||||
<div class="empty" v-if="couponsList.length <= 0">
|
||||
<u-empty text="暂无优惠券" mode="coupon"></u-empty>
|
||||
</div>
|
||||
<view class="coupon-item" v-for="(item, index) in couponsList" :key="index">
|
||||
<view class="left">
|
||||
<view class="wave-line">
|
||||
<view class="wave" v-for="(item, index) in 12" :key="index"></view>
|
||||
</view>
|
||||
<view class="message">
|
||||
<view>
|
||||
<span v-if="item.couponType == 'DISCOUNT'">{{ item.discount }}折</span>
|
||||
<span v-else>{{ item.price }}元</span>
|
||||
</view>
|
||||
<view>满{{ item.consumeThreshold | unitPrice }}元可用</view>
|
||||
</view>
|
||||
<view class="circle circle-top"></view>
|
||||
<view class="circle circle-bottom"></view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<view class="desc">
|
||||
<view v-if="item.scopeType">
|
||||
<span v-if="item.scopeType == 'ALL' && item.storeId == '0'">全平台</span>
|
||||
<span v-if="item.scopeType == 'PORTION_GOODS_CATEGORY'">仅限品类</span>
|
||||
<view v-else
|
||||
>{{
|
||||
item.storeName == "platform" ? "全平台" : item.storeName + "店铺"
|
||||
}}使用</view
|
||||
>
|
||||
</view>
|
||||
<view class="reason" v-if="item.reason">{{ item.reason }}</view>
|
||||
<view class="end-time">有效期至:{{ item.endTime }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="receive"
|
||||
v-if="current == 0 && !routerVal.selectedCoupon.includes(item.id)"
|
||||
@click="clickWay(item)"
|
||||
>
|
||||
<text>立即</text><br />
|
||||
<text>使用</text>
|
||||
</view>
|
||||
<view class="used" v-if="current == 0 && routerVal.selectedCoupon.includes(item.id)" @click="clickWay(item)">
|
||||
<text>取消</text><br />
|
||||
<text>使用</text>
|
||||
</view>
|
||||
<view class="bg-quan">券</view>
|
||||
</view>
|
||||
<view class="b-content" :style="themeStyle">
|
||||
<view class="coupon-tabs">
|
||||
<u-tabs
|
||||
:list="list"
|
||||
:scrollable="false"
|
||||
:inactiveStyle="{ color: '#333' }"
|
||||
v-model:current="current"
|
||||
:lineColor="lightColor"
|
||||
:activeStyle="{ color: lightColor }"
|
||||
></u-tabs>
|
||||
</view>
|
||||
</div>
|
||||
|
||||
<scroll-view class="list-scroll-content" scroll-y>
|
||||
<u-empty
|
||||
mode="coupon"
|
||||
text="暂无优惠券"
|
||||
v-if="couponsList.length <= 0"
|
||||
></u-empty>
|
||||
|
||||
<view
|
||||
class="coupon-card"
|
||||
:class="{ 'coupon-used': current != 0 }"
|
||||
v-for="(item, index) in couponsList"
|
||||
:key="item.id || index"
|
||||
>
|
||||
<view class="coupon-card-left">
|
||||
<text class="coupon-price-symbol" v-if="item.couponType != 'DISCOUNT'">¥</text>
|
||||
<text class="coupon-price-value">
|
||||
{{ item.couponType == 'DISCOUNT' ? item.discount + '折' : unitPrice(item.price) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="coupon-divider"></view>
|
||||
<view class="coupon-card-right">
|
||||
<view class="coupon-info">
|
||||
<text class="coupon-card-name">
|
||||
<template v-if="item.scopeType == 'ALL' && item.storeId == '0'">全平台</template>
|
||||
<template v-else-if="item.scopeType == 'PORTION_GOODS_CATEGORY'">仅限品类</template>
|
||||
<template v-else>
|
||||
{{ item.storeName == 'platform' ? '全平台' : (item.storeName || '') + '店铺' }}
|
||||
</template>
|
||||
使用
|
||||
</text>
|
||||
<text class="coupon-card-desc">满{{ unitPrice(item.consumeThreshold) }}元可用</text>
|
||||
<text class="coupon-card-time" v-if="item.reason">{{ item.reason }}</text>
|
||||
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="coupon-status-btn"
|
||||
v-if="current == 0 && !isSelected(item)"
|
||||
@click="clickWay(item)"
|
||||
>
|
||||
立即使用
|
||||
</view>
|
||||
<view
|
||||
class="coupon-status-btn cancel"
|
||||
v-else-if="current == 0 && isSelected(item)"
|
||||
@click="clickWay(item)"
|
||||
>
|
||||
取消使用
|
||||
</view>
|
||||
<view class="coupon-status-btn disabled" v-else>
|
||||
不可用
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { useCoupon } from "@/api/trade.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lightColor: this.$lightColor,
|
||||
current: 0,
|
||||
list: [
|
||||
{
|
||||
name: "可用优惠券",
|
||||
},
|
||||
{
|
||||
name: "不可用优惠券",
|
||||
},
|
||||
],
|
||||
couponsList: [], //优惠券集合
|
||||
params: {
|
||||
//传参
|
||||
memberCouponStatus: "NEW", //优惠券状态
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
scopeId: "", //商品skuid
|
||||
storeId: "", //店铺id
|
||||
totalPrice: "", //价格
|
||||
},
|
||||
routerVal: "", //上级传参
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.routerVal = options;
|
||||
},
|
||||
watch: {
|
||||
current(val) {
|
||||
console.log(this.$store.state.cantUseCoupons);
|
||||
val == 0
|
||||
? (this.couponsList = this.$store.state.canUseCoupons)
|
||||
: (this.couponsList = this.$store.state.cantUseCoupons);
|
||||
},
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { useCoupon } from '@/api/trade.js'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
console.log(this.routerVal);
|
||||
},
|
||||
const store = useStore()
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 从vuex中拿取优惠券信息
|
||||
*/
|
||||
init() {
|
||||
this.couponsList = this.$store.state.canUseCoupons;
|
||||
},
|
||||
/**
|
||||
* 领取优惠券
|
||||
*/
|
||||
clickWay(coupon) {
|
||||
useCoupon({
|
||||
memberCouponId: coupon.id,
|
||||
used: !this.routerVal.selectedCoupon.includes(coupon.id),
|
||||
way: this.routerVal.way,
|
||||
}).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message,
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const current = ref(0)
|
||||
const list = [
|
||||
{ name: '可用优惠券' },
|
||||
{ name: '不可用优惠券' },
|
||||
]
|
||||
const couponsList = ref<any[]>([])
|
||||
const routerVal = ref<Record<string, any>>({
|
||||
selectedCoupon: [],
|
||||
way: '',
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
const selectedCoupon = options?.selectedCoupon
|
||||
let selected: string[] = []
|
||||
if (Array.isArray(selectedCoupon)) {
|
||||
selected = selectedCoupon
|
||||
} else if (typeof selectedCoupon === 'string' && selectedCoupon) {
|
||||
selected = selectedCoupon.split(',')
|
||||
}
|
||||
routerVal.value = {
|
||||
...(options || {}),
|
||||
selectedCoupon: selected,
|
||||
}
|
||||
})
|
||||
|
||||
watch(current, (val) => {
|
||||
couponsList.value = val == 0
|
||||
? store.state.canUseCoupons
|
||||
: store.state.cantUseCoupons
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
couponsList.value = store.state.canUseCoupons || []
|
||||
})
|
||||
|
||||
function isSelected(coupon: any) {
|
||||
return (routerVal.value.selectedCoupon || []).includes(coupon.id)
|
||||
}
|
||||
|
||||
function clickWay(coupon: any) {
|
||||
useCoupon({
|
||||
memberCouponId: coupon.id,
|
||||
used: !isSelected(coupon),
|
||||
way: routerVal.value.way,
|
||||
}).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message,
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.desc {
|
||||
height: 220rpx;
|
||||
flex: 2;
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.b-content {
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.end-time,
|
||||
.reason {
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
.coupon-tabs {
|
||||
background: #fff;
|
||||
height: 88rpx;
|
||||
box-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wrapper {
|
||||
background: #f9f9f9;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.u-tabs),
|
||||
:deep(.u-tabs__wrapper),
|
||||
:deep(.u-tabs__wrapper__scroll-view-wrapper),
|
||||
:deep(.u-tabs__wrapper__scroll-view),
|
||||
:deep(.u-tabs__wrapper__nav) {
|
||||
background: #fff;
|
||||
height: 88rpx;
|
||||
}
|
||||
.coupon-item {
|
||||
|
||||
:deep(.u-tabs__wrapper__nav__item) {
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
:deep(.u-tabs__wrapper__nav__item__text) {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.list-scroll-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 220rpx;
|
||||
margin: 20rpx;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
border: 1rpx solid #f0f1f5;
|
||||
overflow: hidden;
|
||||
|
||||
.left {
|
||||
height: 100%;
|
||||
width: 260rpx;
|
||||
background-color: $light-color;
|
||||
position: relative;
|
||||
.message {
|
||||
color: $font-color-white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 40rpx;
|
||||
|
||||
view:nth-child(1) {
|
||||
font-weight: bold;
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
view:nth-child(2) {
|
||||
font-size: $font-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-line {
|
||||
height: 220rpx;
|
||||
width: 8rpx;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $light-color;
|
||||
overflow: hidden;
|
||||
|
||||
.wave {
|
||||
width: 8rpx;
|
||||
height: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0 16rpx 16rpx 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
.circle {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: $bg-color;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
z-index: 111;
|
||||
}
|
||||
.circle-top {
|
||||
top: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
.circle-bottom {
|
||||
bottom: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 450rpx;
|
||||
font-size: $font-sm;
|
||||
height: 220rpx;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
> view:nth-child(1) {
|
||||
color: #666666;
|
||||
margin-left: 20rpx;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 50%;
|
||||
left: 204rpx;
|
||||
z-index: 2;
|
||||
box-shadow: inset 0 0 0 1rpx #eceef2;
|
||||
}
|
||||
|
||||
> view:nth-child(1) {
|
||||
color: #ff6262;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
top: -16rpx;
|
||||
}
|
||||
|
||||
.receive {
|
||||
color: #ffffff;
|
||||
background-color: $main-color;
|
||||
border-radius: 50%;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
text-align: center;
|
||||
margin-right: 30rpx;
|
||||
vertical-align: middle;
|
||||
padding-top: 8rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
&::after {
|
||||
bottom: -16rpx;
|
||||
}
|
||||
|
||||
.used {
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
text-align: center;
|
||||
margin-right: 30rpx;
|
||||
vertical-align: middle;
|
||||
padding-top: 8rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
&.coupon-used {
|
||||
.coupon-card-left {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bg-quan {
|
||||
width: 244rpx;
|
||||
height: 244rpx;
|
||||
border: 6rpx solid $main-color;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
color: $main-color;
|
||||
text-align: center;
|
||||
padding-top: 30rpx;
|
||||
font-size: 130rpx;
|
||||
position: absolute;
|
||||
right: -54rpx;
|
||||
bottom: -60rpx;
|
||||
}
|
||||
.coupon-card-left {
|
||||
width: 220rpx;
|
||||
min-height: 180rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #ff3b30;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffecec 100%);
|
||||
}
|
||||
|
||||
.coupon-price-symbol {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-price-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.coupon-divider {
|
||||
position: absolute;
|
||||
left: 220rpx;
|
||||
top: 24rpx;
|
||||
bottom: 24rpx;
|
||||
width: 0;
|
||||
border-left: 2rpx dashed #eceef2;
|
||||
}
|
||||
|
||||
.coupon-card-right {
|
||||
flex: 1;
|
||||
padding: 32rpx 32rpx 32rpx 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.coupon-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.coupon-card-name {
|
||||
font-size: 30rpx;
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.coupon-card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8a8f99;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coupon-card-time {
|
||||
font-size: 22rpx;
|
||||
color: #b0b3bf;
|
||||
}
|
||||
|
||||
.coupon-status-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--theme-light, #ff6b35), var(--theme-primary, #ff4b2b));
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,472 +1,393 @@
|
||||
<template>
|
||||
<view class="b-content">
|
||||
<view class="navbar">
|
||||
<!-- 循环出头部tab栏 -->
|
||||
<view
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
class="nav-item"
|
||||
@click="handleTabClick(index)"
|
||||
><text :class="{ current: tabCurrentIndex === index }">{{
|
||||
item.text
|
||||
}}</text></view
|
||||
>
|
||||
</view>
|
||||
<swiper
|
||||
:current="tabCurrentIndex"
|
||||
class="swiper-box"
|
||||
duration="300"
|
||||
@change="changeTab"
|
||||
>
|
||||
<swiper-item
|
||||
class="tab-content"
|
||||
v-for="(navItem, navIndex) in navList"
|
||||
:key="navIndex"
|
||||
>
|
||||
<scroll-view
|
||||
class="list-scroll-content"
|
||||
scroll-y
|
||||
@scrolltolower="loadData"
|
||||
>
|
||||
<!-- 空白页 -->
|
||||
<u-empty
|
||||
mode="coupon"
|
||||
text="暂无优惠券了"
|
||||
v-if="navItem.whetherEmpty"
|
||||
></u-empty>
|
||||
|
||||
<!-- 数据 -->
|
||||
<view
|
||||
v-if="navItem.dataList && coupon"
|
||||
class="coupon-item"
|
||||
:class="{ 'coupon-used': navIndex != 0 }"
|
||||
v-for="(coupon, index) in navItem.dataList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="left">
|
||||
<view class="wave-line">
|
||||
<view
|
||||
class="wave"
|
||||
v-for="(item, index) in 12"
|
||||
:key="index"
|
||||
></view>
|
||||
</view>
|
||||
<view class="message">
|
||||
<view class="price" v-if="coupon.couponType == 'DISCOUNT'"
|
||||
>{{ coupon.discount }}折</view
|
||||
>
|
||||
<view class="price" v-else>{{ coupon.price }}元</view>
|
||||
<view class="sub-price"
|
||||
>满{{ coupon.consumeThreshold | unitPrice }}可用</view
|
||||
>
|
||||
</view>
|
||||
<view class="circle circle-top"></view>
|
||||
<view class="circle circle-bottom"></view>
|
||||
</view>
|
||||
<view class="right" v-if="coupon">
|
||||
<view class="content">
|
||||
<view class="title-1">{{ coupon.title }}</view>
|
||||
<view class="title-2"
|
||||
>使用范围:{{
|
||||
coupon.scopeType == "ALL" && coupon.storeId == "0"
|
||||
? "全平台"
|
||||
: coupon.scopeType == "PORTION_GOODS"
|
||||
? "部分商品"
|
||||
: coupon.scopeType == "PORTION_GOODS_CATEGORY"
|
||||
? "部分分类商品"
|
||||
: coupon.storeName == "platform"
|
||||
? "全平台"
|
||||
: coupon.storeName + ""
|
||||
}}使用</view
|
||||
>
|
||||
<view v-if="coupon.endTime">{{ coupon.endTime }}</view>
|
||||
<view @click="couponDetail(coupon)"
|
||||
>详细说明
|
||||
<u-icon
|
||||
style="float: right; margin-top: 10rpx"
|
||||
name="arrow-right"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="jiao-1" v-if="navIndex == 0">
|
||||
<text class="text-1">新到</text>
|
||||
<text class="text-2" v-if="coupon.used_status == 1"
|
||||
>将过期</text
|
||||
>
|
||||
</view>
|
||||
<image
|
||||
class="no-icon"
|
||||
v-if="navIndex == 1"
|
||||
src="@/static/img/used.png"
|
||||
></image>
|
||||
<image
|
||||
class="no-icon"
|
||||
v-if="navIndex == 2"
|
||||
src="@/static/img/overdue.png"
|
||||
></image>
|
||||
<view
|
||||
class="receive"
|
||||
v-if="navIndex == 0"
|
||||
@click="useItNow(coupon)"
|
||||
>
|
||||
<text>立即</text><br />
|
||||
<text>使用</text>
|
||||
</view>
|
||||
<view class="bg-quan"> 券 </view>
|
||||
</view>
|
||||
</view>
|
||||
<uni-load-more :status="navItem.loadStatus"></uni-load-more>
|
||||
</scroll-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMemberCoupons } from "@/api/members.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabCurrentIndex: 0, //tab栏下标默认为0 未使用
|
||||
navList: [
|
||||
//每个tab存储的信息
|
||||
{
|
||||
text: "未使用",
|
||||
loadStatus: "more",
|
||||
dataList: [],
|
||||
params: {
|
||||
memberCouponStatus: "NEW",
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 1,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
{
|
||||
text: "已使用",
|
||||
loadStatus: "more",
|
||||
dataList: [],
|
||||
params: {
|
||||
memberCouponStatus: "USED",
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 2,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
{
|
||||
text: "已过期",
|
||||
loadStatus: "more",
|
||||
dataList: [],
|
||||
params: {
|
||||
memberCouponStatus: "EXPIRE",
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 3,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
],
|
||||
couponList: [], //优惠券列表
|
||||
};
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.navList[this.tabCurrentIndex].params.pageNumber = 1;
|
||||
this.navList[this.tabCurrentIndex].dataList = [];
|
||||
this.getData();
|
||||
},
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* 监听切换顶部tab栏实现刷新数据
|
||||
*/
|
||||
tabCurrentIndex(val) {
|
||||
if (this.navList[val].dataList.length == 0) this.getData();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 顶部tab点击
|
||||
*/
|
||||
handleTabClick(index) {
|
||||
this.tabCurrentIndex = index;
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取优惠券
|
||||
*/
|
||||
getData() {
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
});
|
||||
let index = this.tabCurrentIndex;
|
||||
getMemberCoupons(this.navList[index].params).then((res) => {
|
||||
uni.stopPullDownRefresh();
|
||||
if (res.data.success) {
|
||||
let data = res.data.result.records;
|
||||
if (data.length == 0) {
|
||||
if (res.data.pageNumber == 1) {
|
||||
this.navList[index].whetherEmpty = true;
|
||||
} else {
|
||||
this.navList[index].loadStatus = "noMore";
|
||||
}
|
||||
} else if (data.length < 10) {
|
||||
this.navList[index].loadStatus = "noMore";
|
||||
this.navList[index].dataList.push(...data);
|
||||
} else {
|
||||
this.navList[index].dataList.push(...data);
|
||||
}
|
||||
}
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换tab
|
||||
*/
|
||||
changeTab(e) {
|
||||
this.tabCurrentIndex = e.target.current;
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
loadData() {
|
||||
let index = this.tabCurrentIndex;
|
||||
if (this.navList[index].loadStatus != "noMore") {
|
||||
this.navList[index].params.pageNumber++;
|
||||
this.getData();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 立即使用优惠券
|
||||
*/
|
||||
useItNow(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage?promotionsId=${item.couponId}&promotionType=COUPON`,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 优惠券详情
|
||||
*/
|
||||
couponDetail(item) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
"/pages/cart/coupon/couponDetail?item=" +
|
||||
encodeURIComponent(JSON.stringify(item)),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
$item-color: #fff;
|
||||
|
||||
.b-content {
|
||||
background: $page-color-base;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.list-scroll-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 220rpx;
|
||||
margin: 20rpx;
|
||||
|
||||
.left {
|
||||
height: 100%;
|
||||
width: 260rpx;
|
||||
background-color: $light-color;
|
||||
position: relative;
|
||||
|
||||
.message {
|
||||
color: $font-color-white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 40rpx;
|
||||
|
||||
view:nth-child(1) {
|
||||
font-weight: bold;
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
view:nth-child(2) {
|
||||
font-size: $font-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-line {
|
||||
height: 220rpx;
|
||||
width: 8rpx;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $light-color;
|
||||
overflow: hidden;
|
||||
|
||||
.wave {
|
||||
width: 8rpx;
|
||||
height: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0 16rpx 16rpx 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background-color: $bg-color;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
z-index: 111;
|
||||
}
|
||||
|
||||
.circle-top {
|
||||
top: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
|
||||
.circle-bottom {
|
||||
bottom: -20rpx;
|
||||
right: -20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 450rpx;
|
||||
font-size: $font-sm;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
color: #666666;
|
||||
margin-left: 20rpx;
|
||||
line-height: 2em;
|
||||
> view:nth-child(1) {
|
||||
color: #ff6262;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.title-1,
|
||||
.title-2,
|
||||
.title-3 {
|
||||
font-size: 25rpx;
|
||||
}
|
||||
}
|
||||
.receive {
|
||||
color: #ffffff;
|
||||
background-color: $main-color;
|
||||
border-radius: 50%;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
text-align: center;
|
||||
margin-right: 48rpx;
|
||||
vertical-align: middle;
|
||||
padding-top: 8rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.jiao-1 {
|
||||
background-color: #ffc71c;
|
||||
width: 400rpx;
|
||||
transform: rotate(45deg);
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
color: #ffffff;
|
||||
right: -130rpx;
|
||||
top: 0;
|
||||
.text-1 {
|
||||
margin-left: 68rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.text-2 {
|
||||
margin-left: 68rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
.no-icon {
|
||||
border-radius: 50%;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
margin-right: 48rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.bg-quan {
|
||||
width: 244rpx;
|
||||
height: 244rpx;
|
||||
border: 6rpx solid $main-color;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
color: $main-color;
|
||||
text-align: center;
|
||||
padding-top: 30rpx;
|
||||
font-size: 130rpx;
|
||||
position: absolute;
|
||||
right: -54rpx;
|
||||
bottom: -60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
height: 80rpx;
|
||||
padding: 0 5px;
|
||||
background: #fff;
|
||||
color: $light-color;
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 26rpx;
|
||||
color: $light-color;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text {
|
||||
line-height: 80rpx;
|
||||
}
|
||||
.current {
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 10rpx;
|
||||
left: 108rpx;
|
||||
width: 30rpx;
|
||||
border-bottom: 2px solid $light-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="b-content">
|
||||
<view class="coupon-tabs">
|
||||
<u-tabs
|
||||
:list="navList"
|
||||
keyName="text"
|
||||
:scrollable="false"
|
||||
:inactiveStyle="{ color: '#333' }"
|
||||
v-model:current="tabCurrentIndex"
|
||||
:lineColor="lightColor"
|
||||
:activeStyle="{ color: lightColor }"
|
||||
></u-tabs>
|
||||
</view>
|
||||
<swiper
|
||||
:current="tabCurrentIndex"
|
||||
class="swiper-box"
|
||||
duration="300"
|
||||
@change="changeTab"
|
||||
>
|
||||
<swiper-item
|
||||
class="tab-content"
|
||||
v-for="(navItem, navIndex) in navList"
|
||||
:key="navIndex"
|
||||
>
|
||||
<scroll-view
|
||||
class="list-scroll-content"
|
||||
scroll-y
|
||||
@scrolltolower="loadData"
|
||||
>
|
||||
<u-empty
|
||||
mode="coupon"
|
||||
text="暂无优惠券了"
|
||||
v-if="navItem.whetherEmpty"
|
||||
></u-empty>
|
||||
|
||||
<view
|
||||
class="coupon-card"
|
||||
:class="{ 'coupon-used': navIndex != 0, 'coupon-expired': navIndex == 2 }"
|
||||
v-for="(coupon, index) in navItem.dataList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="coupon-card-left">
|
||||
<text class="coupon-price-symbol" v-if="coupon.couponType != 'DISCOUNT'">¥</text>
|
||||
<text class="coupon-price-value">{{ coupon.couponType == 'DISCOUNT' ? coupon.discount + '折' : unitPrice(coupon.price) }}</text>
|
||||
</view>
|
||||
<view class="coupon-divider"></view>
|
||||
<view class="coupon-card-right">
|
||||
<view class="coupon-info">
|
||||
<text class="coupon-card-name">{{ coupon.title }}</text>
|
||||
<text class="coupon-card-desc">使用范围:{{
|
||||
coupon.scopeType == "ALL" && coupon.storeId == "0"
|
||||
? "全平台"
|
||||
: coupon.scopeType == "PORTION_GOODS"
|
||||
? "部分商品"
|
||||
: coupon.scopeType == "PORTION_GOODS_CATEGORY"
|
||||
? "部分分类商品"
|
||||
: coupon.storeName == "platform"
|
||||
? "全平台"
|
||||
: coupon.storeName + ""
|
||||
}}使用</text>
|
||||
<text class="coupon-card-time" v-if="coupon.endTime">{{ coupon.endTime }}</text>
|
||||
</view>
|
||||
<view class="coupon-status-btn" v-if="navIndex == 0" @click="useItNow(coupon)">
|
||||
立即使用
|
||||
</view>
|
||||
<view class="coupon-status-btn disabled" v-else-if="navIndex == 1">
|
||||
已使用
|
||||
</view>
|
||||
<view class="coupon-status-btn disabled" v-else>
|
||||
已过期
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<uni-load-more :status="navItem.loadStatus"></uni-load-more>
|
||||
</scroll-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getMemberCoupons } from '@/api/members.js'
|
||||
import { useStore } from '@/store'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const tabCurrentIndex = ref(0)
|
||||
const navList = ref([
|
||||
{
|
||||
text: '未使用',
|
||||
loadStatus: 'more',
|
||||
dataList: [] as any[],
|
||||
params: {
|
||||
memberCouponStatus: 'NEW',
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 1,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
{
|
||||
text: '已使用',
|
||||
loadStatus: 'more',
|
||||
dataList: [] as any[],
|
||||
params: {
|
||||
memberCouponStatus: 'USED',
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 2,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
{
|
||||
text: '已过期',
|
||||
loadStatus: 'more',
|
||||
dataList: [] as any[],
|
||||
params: {
|
||||
memberCouponStatus: 'EXPIRE',
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
status: 3,
|
||||
},
|
||||
whetherEmpty: false,
|
||||
},
|
||||
])
|
||||
|
||||
onShow(() => {
|
||||
navList.value[tabCurrentIndex.value].params.pageNumber = 1
|
||||
navList.value[tabCurrentIndex.value].dataList = []
|
||||
getData()
|
||||
})
|
||||
|
||||
watch(tabCurrentIndex, (val) => {
|
||||
if (navList.value[val].dataList.length == 0) getData()
|
||||
})
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
function handleTabClick(index: number) {
|
||||
tabCurrentIndex.value = index
|
||||
}
|
||||
|
||||
function getData() {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
const index = tabCurrentIndex.value
|
||||
getMemberCoupons(navList.value[index].params).then((res) => {
|
||||
uni.stopPullDownRefresh()
|
||||
if (res.data.success) {
|
||||
const data = res.data.result.records
|
||||
if (data.length == 0) {
|
||||
if (res.data.pageNumber == 1) {
|
||||
navList.value[index].whetherEmpty = true
|
||||
} else {
|
||||
navList.value[index].loadStatus = 'noMore'
|
||||
}
|
||||
} else if (data.length < 10) {
|
||||
navList.value[index].loadStatus = 'noMore'
|
||||
navList.value[index].dataList.push(...data)
|
||||
} else {
|
||||
navList.value[index].dataList.push(...data)
|
||||
}
|
||||
}
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function changeTab(e: any) {
|
||||
tabCurrentIndex.value = e.detail.current
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
const index = tabCurrentIndex.value
|
||||
if (navList.value[index].loadStatus != 'noMore') {
|
||||
navList.value[index].params.pageNumber++
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
function useItNow(item: any) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage?promotionsId=${item.couponId}&promotionType=COUPON`,
|
||||
})
|
||||
}
|
||||
|
||||
function couponDetail(item: any) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/cart/coupon/couponDetail?item=' + encodeURIComponent(JSON.stringify(item)),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.b-content {
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.coupon-tabs {
|
||||
background: #fff;
|
||||
height: 88rpx;
|
||||
box-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:deep(.u-tabs),
|
||||
:deep(.u-tabs__wrapper),
|
||||
:deep(.u-tabs__wrapper__scroll-view-wrapper),
|
||||
:deep(.u-tabs__wrapper__scroll-view),
|
||||
:deep(.u-tabs__wrapper__nav) {
|
||||
background: #fff;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
:deep(.u-tabs__wrapper__nav__item) {
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
:deep(.u-tabs__wrapper__nav__item__text) {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
height: calc(100vh - 88rpx - var(--status-bar-height));
|
||||
}
|
||||
|
||||
.list-scroll-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
border: 1rpx solid #f0f1f5;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 50%;
|
||||
left: 204rpx;
|
||||
z-index: 2;
|
||||
box-shadow: inset 0 0 0 1rpx #eceef2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16rpx;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -16rpx;
|
||||
}
|
||||
|
||||
&.coupon-used {
|
||||
.coupon-card-left {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
||||
color: #999;
|
||||
}
|
||||
.coupon-status-btn {
|
||||
background: #ccc;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.coupon-expired {
|
||||
.coupon-card-left {
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
||||
color: #999;
|
||||
}
|
||||
.coupon-status-btn {
|
||||
background: #ccc;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-card-left {
|
||||
width: 220rpx;
|
||||
min-height: 180rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #ff3b30;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffecec 100%);
|
||||
}
|
||||
|
||||
.coupon-price-symbol {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.coupon-price-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.coupon-divider {
|
||||
position: absolute;
|
||||
left: 220rpx;
|
||||
top: 24rpx;
|
||||
bottom: 24rpx;
|
||||
width: 0;
|
||||
border-left: 2rpx dashed #eceef2;
|
||||
}
|
||||
|
||||
.coupon-card-right {
|
||||
flex: 1;
|
||||
padding: 32rpx 32rpx 32rpx 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.coupon-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.coupon-card-name {
|
||||
font-size: 30rpx;
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.coupon-card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8a8f99;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.coupon-card-time {
|
||||
font-size: 22rpx;
|
||||
color: #b0b3bf;
|
||||
}
|
||||
|
||||
.coupon-status-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff6b35, #ff4b2b);
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
209
pages/cart/gift-card/index.vue
Normal file
209
pages/cart/gift-card/index.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<view class="b-content" :style="themeStyle">
|
||||
<scroll-view class="list-scroll-content" scroll-y>
|
||||
<u-empty
|
||||
mode="coupon"
|
||||
text="暂无可用礼品卡"
|
||||
v-if="!cardList.length"
|
||||
></u-empty>
|
||||
|
||||
<view class="coupon-card" v-for="item in cardList" :key="item.id">
|
||||
<view class="coupon-card-left">
|
||||
<view class="price-row">
|
||||
<text class="coupon-price-symbol">¥</text>
|
||||
<text class="coupon-price-value">{{ unitPrice(item.balance) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-divider"></view>
|
||||
<view class="coupon-card-right">
|
||||
<text class="coupon-card-name">{{ item.giftCardName || '礼品卡' }}</text>
|
||||
<view class="coupon-card-foot">
|
||||
<view
|
||||
class="coupon-status-btn"
|
||||
:class="{ cancel: item.selected }"
|
||||
@click="clickWay(item)"
|
||||
>
|
||||
{{ item.selected ? '取消使用' : '立即使用' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="notice" v-if="cardList.length">{{ GIFT_CARD_PAY_NOTICE }}</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { selectGiftCard } from '@/api/trade.js'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { decorateGiftCard, GIFT_CARD_PAY_NOTICE } from '@/utils/giftCard.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const way = ref('')
|
||||
const cardList = ref<any[]>([])
|
||||
|
||||
onLoad((options) => {
|
||||
way.value = options?.way || ''
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const selectedIds = (store.state as any).selectedGiftCardIds || []
|
||||
const cards = (store.state as any).canUseGiftCards || []
|
||||
cardList.value = cards.map((item: any) => decorateGiftCard(item, selectedIds))
|
||||
})
|
||||
|
||||
function clickWay(item: any) {
|
||||
if (!item?.id) return
|
||||
selectGiftCard({
|
||||
way: way.value,
|
||||
credentialId: String(item.id),
|
||||
used: !item.selected,
|
||||
}).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.data.message || '礼品卡选择失败',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.b-content {
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-scroll-content {
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.coupon-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
border: 1rpx solid #f0f1f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.coupon-card-left {
|
||||
width: 200rpx;
|
||||
min-height: 148rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: #ff3b30;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffecec 100%);
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coupon-price-symbol {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
margin-right: 2rpx;
|
||||
}
|
||||
|
||||
.coupon-price-value {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.coupon-price-label {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ff8a80;
|
||||
}
|
||||
|
||||
.coupon-divider {
|
||||
position: absolute;
|
||||
left: 200rpx;
|
||||
top: 24rpx;
|
||||
bottom: 24rpx;
|
||||
width: 0;
|
||||
border-left: 2rpx dashed #f0d6d6;
|
||||
}
|
||||
|
||||
.coupon-card-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 24rpx 24rpx 20rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.coupon-card-name {
|
||||
font-size: 28rpx;
|
||||
color: #111;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.coupon-card-desc,
|
||||
.coupon-card-time {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8a8f99;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.coupon-card-time {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.coupon-card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.coupon-status-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--theme-light, #ff6b35), var(--theme-primary, #ff4b2b));
|
||||
|
||||
&.cancel {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
431
pages/cart/gift-card/myGiftCards.vue
Normal file
431
pages/cart/gift-card/myGiftCards.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="bind-section">
|
||||
<view class="section-title">兑换礼品卡</view>
|
||||
<view class="bind-row">
|
||||
<text class="bind-label">卡号</text>
|
||||
<u-input
|
||||
v-model="bindForm.cardNo"
|
||||
border="none"
|
||||
maxlength="64"
|
||||
placeholder="请输入卡号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="bind-row">
|
||||
<text class="bind-label">兑换码</text>
|
||||
<u-input
|
||||
v-model="bindForm.cardSecret"
|
||||
border="none"
|
||||
maxlength="128"
|
||||
placeholder="请输入兑换码(卡密)"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="bind-btn" :class="{ disabled: bindSubmitting }" @click="submitBind">
|
||||
{{ bindSubmitting ? '兑换中...' : '兑换' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs-wrap">
|
||||
<u-tabs
|
||||
:list="tabList"
|
||||
keyName="text"
|
||||
:scrollable="false"
|
||||
:inactiveStyle="{ color: '#333' }"
|
||||
v-model:current="tabCurrentIndex"
|
||||
:lineColor="lightColor"
|
||||
:activeStyle="{ color: lightColor }"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<scroll-view class="list-scroll" scroll-y @scrolltolower="loadMore">
|
||||
<u-empty v-if="!loading && !list.length" mode="coupon" text="暂无礼品卡" />
|
||||
<view
|
||||
class="gcc-card"
|
||||
:class="{ 'is-disabled': statusTab === 'UNAVAILABLE' }"
|
||||
v-for="item in list"
|
||||
:key="item.id || item.cardNo"
|
||||
>
|
||||
<view class="gcc-header">
|
||||
<view class="gcc-header-pattern" />
|
||||
<view class="gcc-header-inner">
|
||||
<view class="gcc-header-left">
|
||||
<view class="gcc-title">{{ item.giftCardName || '礼品卡' }}</view>
|
||||
<view class="gcc-face">面值{{ item.faceText }}元</view>
|
||||
<view class="gcc-cardno">{{ item.cardNo }}</view>
|
||||
</view>
|
||||
<view class="gcc-header-right">
|
||||
<view class="gcc-type">现金卡</view>
|
||||
<view class="gcc-expire">{{ item.expireText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gcc-body">
|
||||
<view class="gcc-row">
|
||||
当前余额:
|
||||
<text class="gcc-strong" v-if="item.balance != null">¥{{ unitPrice(item.balance) }}</text>
|
||||
<text class="gcc-strong" v-else>—</text>
|
||||
</view>
|
||||
<view
|
||||
class="gcc-action"
|
||||
:class="{ 'is-disabled': actionDisabled || isActivating(item) }"
|
||||
@click="onCardPrimaryAction(item)"
|
||||
>
|
||||
<text v-if="isActivating(item)">提交中...</text>
|
||||
<text v-else>{{ actionLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-more" v-if="list.length">{{ loadStatusText }}</view>
|
||||
<view class="notice">{{ useNotice }}</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
activateGiftCardCashMemberCard,
|
||||
bindGiftCardCashMemberCard,
|
||||
getGiftCardCashMemberCardPage,
|
||||
} from '@/api/promotions.js'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import {
|
||||
decorateGiftCard,
|
||||
GIFT_CARD_STATUS,
|
||||
GIFT_CARD_USE_NOTICE,
|
||||
giftCardActionLabel,
|
||||
resolveMemberCardId,
|
||||
} from '@/utils/giftCard.js'
|
||||
import { useStore } from '@/store'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const store = useStore()
|
||||
const lightColor = computed(() => store.getters.lightColor)
|
||||
const useNotice = GIFT_CARD_USE_NOTICE
|
||||
|
||||
const tabList = [
|
||||
{ text: '可用', status: GIFT_CARD_STATUS.AVAILABLE },
|
||||
{ text: '不可用', status: GIFT_CARD_STATUS.UNAVAILABLE },
|
||||
{ text: '待激活', status: GIFT_CARD_STATUS.PENDING_ACTIVATION },
|
||||
]
|
||||
const tabCurrentIndex = ref(0)
|
||||
const statusTab = computed(() => tabList[tabCurrentIndex.value].status)
|
||||
const actionLabel = computed(() => giftCardActionLabel(statusTab.value))
|
||||
const actionDisabled = computed(() => statusTab.value === GIFT_CARD_STATUS.UNAVAILABLE)
|
||||
|
||||
const bindForm = ref({ cardNo: '', cardSecret: '' })
|
||||
const bindSubmitting = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const pageNumber = ref(1)
|
||||
const loadStatus = ref('more')
|
||||
const activatingId = ref('')
|
||||
|
||||
const loadStatusText = computed(() => (loadStatus.value === 'noMore' ? '没有更多了' : ''))
|
||||
|
||||
onShow(() => {
|
||||
resetAndLoad()
|
||||
})
|
||||
|
||||
watch(tabCurrentIndex, () => {
|
||||
resetAndLoad()
|
||||
})
|
||||
|
||||
function resetAndLoad() {
|
||||
pageNumber.value = 1
|
||||
list.value = []
|
||||
loadStatus.value = 'more'
|
||||
getList()
|
||||
}
|
||||
|
||||
function getList() {
|
||||
if (loadStatus.value === 'noMore' && pageNumber.value > 1) return
|
||||
loading.value = true
|
||||
getGiftCardCashMemberCardPage({
|
||||
pageNumber: pageNumber.value,
|
||||
pageSize: 10,
|
||||
sort: 'createTime',
|
||||
order: 'desc',
|
||||
memberCardStatus: statusTab.value,
|
||||
}).then((res) => {
|
||||
loading.value = false
|
||||
if (!res.data.success) return
|
||||
const records = ((res.data.result && res.data.result.records) || []).map((item: any) =>
|
||||
decorateGiftCard(item)
|
||||
)
|
||||
if (pageNumber.value === 1) {
|
||||
list.value = records
|
||||
} else {
|
||||
list.value = list.value.concat(records)
|
||||
}
|
||||
loadStatus.value = records.length < 10 ? 'noMore' : 'more'
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (loadStatus.value === 'noMore' || loading.value) return
|
||||
pageNumber.value += 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function submitBind() {
|
||||
const cardNo = String(bindForm.value.cardNo || '').trim()
|
||||
const cardSecret = String(bindForm.value.cardSecret || '').trim()
|
||||
if (!cardNo) {
|
||||
uni.showToast({ title: '请输入卡号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!cardSecret) {
|
||||
uni.showToast({ title: '请输入兑换码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (bindSubmitting.value) return
|
||||
bindSubmitting.value = true
|
||||
bindGiftCardCashMemberCard({ cardNo, cardSecret })
|
||||
.then((res) => {
|
||||
bindSubmitting.value = false
|
||||
if (!res.data.success) return
|
||||
uni.showToast({ title: res.data.message || '兑换成功', icon: 'none' })
|
||||
bindForm.value = { cardNo: '', cardSecret: '' }
|
||||
const pendingIndex = tabList.findIndex((item) => item.status === GIFT_CARD_STATUS.PENDING_ACTIVATION)
|
||||
if (pendingIndex >= 0 && tabCurrentIndex.value !== pendingIndex) {
|
||||
tabCurrentIndex.value = pendingIndex
|
||||
return
|
||||
}
|
||||
resetAndLoad()
|
||||
})
|
||||
.catch(() => {
|
||||
bindSubmitting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function isActivating(item: any) {
|
||||
const id = resolveMemberCardId(item)
|
||||
return !!activatingId.value && activatingId.value === id
|
||||
}
|
||||
|
||||
function onCardPrimaryAction(item: any) {
|
||||
if (actionDisabled.value) return
|
||||
if (statusTab.value === GIFT_CARD_STATUS.PENDING_ACTIVATION) {
|
||||
const memberCardId = resolveMemberCardId(item)
|
||||
if (!memberCardId) {
|
||||
uni.showToast({ title: '卡片信息异常,无法激活', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (activatingId.value) return
|
||||
activatingId.value = memberCardId
|
||||
activateGiftCardCashMemberCard(memberCardId)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({ title: res.data.message || '激活成功', icon: 'none' })
|
||||
const availableIndex = tabList.findIndex((item) => item.status === GIFT_CARD_STATUS.AVAILABLE)
|
||||
if (availableIndex >= 0 && tabCurrentIndex.value !== availableIndex) {
|
||||
tabCurrentIndex.value = availableIndex
|
||||
return
|
||||
}
|
||||
resetAndLoad()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
activatingId.value = ''
|
||||
})
|
||||
return
|
||||
}
|
||||
if (statusTab.value === GIFT_CARD_STATUS.AVAILABLE) {
|
||||
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
background: #f7f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bind-section {
|
||||
margin: 24rpx;
|
||||
padding: 28rpx 24rpx 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.bind-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
border-bottom: 1rpx solid #f0f1f5;
|
||||
}
|
||||
|
||||
.bind-label {
|
||||
width: 110rpx;
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bind-btn {
|
||||
margin-top: 24rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #ff6b35, #ff4b2b);
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrap {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gcc-card {
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
|
||||
.gcc-header {
|
||||
position: relative;
|
||||
min-height: 180rpx;
|
||||
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
|
||||
}
|
||||
|
||||
.gcc-header-pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.22;
|
||||
pointer-events: none;
|
||||
background-image: repeating-linear-gradient(
|
||||
-36deg,
|
||||
transparent,
|
||||
transparent 10rpx,
|
||||
rgba(255, 255, 255, 0.45) 10rpx,
|
||||
rgba(255, 255, 255, 0.45) 12rpx
|
||||
);
|
||||
}
|
||||
|
||||
.gcc-header-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 24rpx 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.gcc-header-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
|
||||
.gcc-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gcc-face {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.gcc-cardno {
|
||||
margin-top: 28rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.75;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.gcc-header-right {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gcc-type {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gcc-expire {
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.gcc-body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.gcc-row {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gcc-strong {
|
||||
color: #ff6b22;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gcc-action {
|
||||
margin-top: 24rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #e95b6c;
|
||||
background: #faf4f5;
|
||||
|
||||
&.is-disabled {
|
||||
color: #c5c8ce;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more,
|
||||
.notice {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
padding: 8rpx 8rpx 40rpx;
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<view class="error-page">
|
||||
<u-navbar title="支付失败" :border="false" :fixed="true" :placeholder="true" :auto-back="true"></u-navbar>
|
||||
<view class="error-content">
|
||||
<u-icon name="close-circle-fill" color="#f56c6c" size="120"></u-icon>
|
||||
<text class="error-title">支付失败</text>
|
||||
<text class="error-desc">{{ errorMessage || '支付过程中出现错误,请重试' }}</text>
|
||||
<view class="btn-group">
|
||||
<u-button type="primary" text="返回订单" @click="goBack"></u-button>
|
||||
<u-button type="warning" text="去支付" @click="rePay"></u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const errorMessage = ref('')
|
||||
const orderSn = ref('')
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options) {
|
||||
errorMessage.value = options.message || ''
|
||||
orderSn.value = options.orderSn || ''
|
||||
}
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function rePay() {
|
||||
if (orderSn.value) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/cart/payment/payOrder?orderSn=${orderSn.value}`
|
||||
})
|
||||
} else {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 40rpx;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.error-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 60rpx;
|
||||
width: 100%;
|
||||
|
||||
:deep(.u-button) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,496 +1,430 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div :style="themeStyle" class="wrapper">
|
||||
<div class="box">
|
||||
<div class="block block-1">
|
||||
<image class="img" src="@/pages/cart/static/pay.png" />
|
||||
<p class="ptips">收银台</p>
|
||||
|
||||
<p class="ptips">剩余支付时间:
|
||||
<u-count-down :show-days="false" :show-border="true" font-size="28" color="#008ffa" border-color="#008ffa" ref="uCountDown" :timestamp="autoCancel"></u-count-down>
|
||||
</p>
|
||||
<p class="ptips">
|
||||
<view class="ptips ptips-countdown">
|
||||
剩余支付时间:
|
||||
<u-count-down
|
||||
v-if="autoCancelTime > 0"
|
||||
:time="autoCancelTime"
|
||||
format="HH:mm:ss"
|
||||
@finish="onPayTimeout"
|
||||
>
|
||||
<template #default="{ hours, minutes, seconds }">
|
||||
<view class="countdown-box">
|
||||
<text class="countdown-num">{{ padTime(hours) }}</text>
|
||||
<text class="countdown-colon">:</text>
|
||||
<text class="countdown-num">{{ padTime(minutes) }}</text>
|
||||
<text class="countdown-colon">:</text>
|
||||
<text class="countdown-num">{{ padTime(seconds) }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</u-count-down>
|
||||
<view v-else class="countdown-box">
|
||||
<text class="countdown-num">00</text>
|
||||
<text class="countdown-colon">:</text>
|
||||
<text class="countdown-num">00</text>
|
||||
<text class="countdown-colon">:</text>
|
||||
<text class="countdown-num">00</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ptips ptips-price">
|
||||
支付金额
|
||||
<span>¥{{ cashierParams.price | unitPrice }}</span>
|
||||
</p>
|
||||
<text class="price-text">¥{{ unitPrice(cashierParams.price) }}</text>
|
||||
</view>
|
||||
</div>
|
||||
</div>
|
||||
<div class="__pay_form__">
|
||||
</div>
|
||||
<div class="block-4" v-if="cashierParams.price > 0">
|
||||
<div class="payItem">支付方式</div>
|
||||
<div class="payItem" v-for="(item, index) in payList" :key="index">
|
||||
<u-row class="row">
|
||||
<div class="col1" @click="awaitPay(item, index)" size="100" style="text-align:left;">
|
||||
<div v-if="item == 'ALIPAY'">
|
||||
<u-icon class="method_icon" name="zhifubao-circle-fill" color="#008ffa" size="80"></u-icon>
|
||||
<span class="method_name">支付宝</span>
|
||||
</div>
|
||||
<div v-if="item == 'WECHAT'">
|
||||
<u-icon class="method_icon" name="weixin-circle-fill" color="#00c98b" size="80"></u-icon>
|
||||
<span class="method_name">微信</span>
|
||||
</div>
|
||||
<div v-if="item == 'WALLET'">
|
||||
<u-icon class="method_icon" name="red-packet-fill" color="#dd6161" size="80"></u-icon>
|
||||
<span class="method_name">余额支付(当前余额:¥{{ walletValue | unitPrice }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col3" @click="awaitPay(item)" textAlign="right">
|
||||
<u-icon size="26" color="#b1b1b1" name="arrow-right"></u-icon>
|
||||
</div>
|
||||
</u-row>
|
||||
<div class="payItem payItem-title">支付方式</div>
|
||||
<div
|
||||
class="payItem payItem-method"
|
||||
v-for="(item, index) in payList"
|
||||
:key="index"
|
||||
@click="awaitPay(item, index)"
|
||||
>
|
||||
<view class="method-left">
|
||||
<template v-if="item == 'ALIPAY'">
|
||||
<u-icon class="method_icon" name="zhifubao-circle-fill" color="#008ffa" size="44"></u-icon>
|
||||
<text class="method_name">支付宝</text>
|
||||
</template>
|
||||
<template v-if="item == 'WECHAT'">
|
||||
<u-icon class="method_icon" name="weixin-circle-fill" color="#00c98b" size="44"></u-icon>
|
||||
<text class="method_name">微信</text>
|
||||
</template>
|
||||
<template v-if="item == 'WALLET'">
|
||||
<u-icon class="method_icon" name="red-packet-fill" color="#dd6161" size="44"></u-icon>
|
||||
<text class="method_name">余额支付(当前余额:¥{{ unitPrice(walletValue) }})</text>
|
||||
</template>
|
||||
</view>
|
||||
<u-icon size="28" color="#b1b1b1" name="arrow-right"></u-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import * as API_Trade from "@/api/trade";
|
||||
import {payCallback} from '@/api/members'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
//路径传参
|
||||
routerVal: "",
|
||||
//收银台参数
|
||||
cashierParams: "",
|
||||
//支付方式集合
|
||||
payList: "",
|
||||
//支付sn
|
||||
sn: "",
|
||||
//订单类型
|
||||
orderType: "",
|
||||
//支付异常
|
||||
exception: {},
|
||||
//支付表单
|
||||
payForm: {},
|
||||
//支付类型 APP/WECHAT_MP/H5/NATIVE app/微信小程序/h5/二维码
|
||||
paymentType: "",
|
||||
// 支付客户端 APP/NATIVE/JSAPI/H5
|
||||
paymentClient: "",
|
||||
//余额
|
||||
walletValue: 0.0,
|
||||
// 支付倒计时
|
||||
autoCancel: 0,
|
||||
|
||||
};
|
||||
},
|
||||
onLoad(val) {
|
||||
this.routerVal = val;
|
||||
<script setup lang="ts">
|
||||
import { ref, getCurrentInstance, onMounted, computed } from 'vue'
|
||||
import { onLoad, onBackPress } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import * as API_Trade from '@/api/trade'
|
||||
import { payCallback } from '@/api/members'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
//初始化参数
|
||||
// #ifdef APP-PLUS
|
||||
this.paymentType = "APP";
|
||||
this.paymentClient = "APP";
|
||||
//#endif
|
||||
// #ifdef MP-WEIXIN
|
||||
this.paymentType = "WECHAT_MP";
|
||||
this.paymentClient = "MP";
|
||||
//#endif
|
||||
// #ifdef H5
|
||||
this.paymentType = "H5";
|
||||
//如果是微信浏览器,则使用公众号支付,否则使用h5,
|
||||
// 区别是:h5是通过浏览器外部调用微信app进行支付,而JSAPI则是 在微信浏览器内部,或者小程序 调用微信支付
|
||||
this.paymentClient = this.isWeiXin() ? "JSAPI" : "H5";
|
||||
//#endif
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const cashierParams = ref<Record<string, any>>({ price: 0 })
|
||||
const payList = ref<string[]>([])
|
||||
const sn = ref('')
|
||||
const orderType = ref('')
|
||||
const paymentType = ref('')
|
||||
const paymentClient = ref('')
|
||||
const walletValue = ref(0)
|
||||
const autoCancelTime = ref(0)
|
||||
|
||||
onLoad((val) => {
|
||||
routerVal.value = val || {}
|
||||
// #ifdef APP-PLUS
|
||||
paymentType.value = 'APP'
|
||||
paymentClient.value = 'APP'
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
paymentType.value = 'WECHAT_MP'
|
||||
paymentClient.value = 'MP'
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
paymentType.value = 'H5'
|
||||
paymentClient.value = isWeiXin() ? 'JSAPI' : 'H5'
|
||||
// #endif
|
||||
})
|
||||
|
||||
//
|
||||
},
|
||||
onBackPress(e) {
|
||||
if (e.from == "backbutton") {
|
||||
if(this.routerVal.recharge_sn){
|
||||
uni.switchTab({
|
||||
url: '/pages/tabbar/user/my'
|
||||
});
|
||||
}
|
||||
else{
|
||||
uni.navigateTo({
|
||||
url: "/pages/order/myOrder?status=0",
|
||||
});
|
||||
}
|
||||
return true; //阻止默认返回行为
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.cashierData();
|
||||
},
|
||||
methods: {
|
||||
onBackPress((e) => {
|
||||
if (e.from == 'backbutton') {
|
||||
if (routerVal.value.recharge_sn) {
|
||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/order/myOrder?status=0' })
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
/**
|
||||
* 支付成功后跳转
|
||||
*/
|
||||
callback(paymentMethod){
|
||||
uni.navigateTo({
|
||||
url: "/pages/cart/payment/success?paymentMethod=" +
|
||||
paymentMethod +
|
||||
"&payPrice=" +
|
||||
this.cashierParams.price+
|
||||
"&orderType="+this.orderType
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收银详情
|
||||
*/
|
||||
cashierData() {
|
||||
let parms = {};
|
||||
onMounted(() => {
|
||||
cashierData()
|
||||
})
|
||||
|
||||
if (this.routerVal.recharge_sn) {
|
||||
// 判断当前是否是充值
|
||||
this.sn = this.routerVal.recharge_sn;
|
||||
this.orderType = "RECHARGE";
|
||||
|
||||
} else if (this.routerVal.trade_sn) {
|
||||
this.sn = this.routerVal.trade_sn;
|
||||
this.orderType = "TRADE";
|
||||
} else {
|
||||
this.sn = this.routerVal.order_sn;
|
||||
this.orderType = "ORDER";
|
||||
}
|
||||
parms.sn = this.sn;
|
||||
parms.orderType = this.orderType;
|
||||
parms.clientType = this.paymentType;
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
API_Trade.getCashierData(parms).then((res) => {
|
||||
|
||||
if(res.data.success){
|
||||
this.cashierParams = res.data.result;
|
||||
function callback(paymentMethod: string) {
|
||||
const goSuccess = () => {
|
||||
uni.redirectTo({
|
||||
url:
|
||||
'/pages/cart/payment/success?paymentMethod=' +
|
||||
paymentMethod +
|
||||
'&payPrice=' +
|
||||
cashierParams.value.price +
|
||||
'&orderType=' +
|
||||
orderType.value +
|
||||
'&orderPromotionType=' +
|
||||
(cashierParams.value.orderPromotionType || ''),
|
||||
})
|
||||
}
|
||||
goSuccess()
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
this.payList = res.data.result.support.filter((item) => {
|
||||
return item != "ALIPAY";
|
||||
});
|
||||
// #endif
|
||||
function cashierData() {
|
||||
const parms: Record<string, string> = {}
|
||||
|
||||
|
||||
if(this.routerVal.recharge_sn){
|
||||
this.payList = res.data.result.support.filter((item) => {
|
||||
return item != "WALLET";
|
||||
})
|
||||
}
|
||||
else{
|
||||
this.payList = res.data.result.support;
|
||||
}
|
||||
// #ifdef H5
|
||||
//判断是否微信浏览器
|
||||
var ua = window.navigator.userAgent.toLowerCase();
|
||||
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
|
||||
|
||||
this.payList = res.data.result.support.filter((item) => {
|
||||
return item != "ALIPAY";
|
||||
});
|
||||
// 充值的话仅保留微信支付
|
||||
if(this.orderType == "RECHARGE"){
|
||||
this.payList = res.data.result.support.filter((item) => {
|
||||
return item == "WECHAT";
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
|
||||
if (routerVal.value.recharge_sn) {
|
||||
sn.value = routerVal.value.recharge_sn
|
||||
orderType.value = 'RECHARGE'
|
||||
} else if (routerVal.value.trade_sn) {
|
||||
sn.value = routerVal.value.trade_sn
|
||||
orderType.value = 'TRADE'
|
||||
} else {
|
||||
sn.value = routerVal.value.order_sn
|
||||
orderType.value = 'ORDER'
|
||||
}
|
||||
parms.sn = sn.value
|
||||
parms.orderType = orderType.value
|
||||
parms.clientType = paymentType.value
|
||||
|
||||
this.walletValue = res.data.result.walletValue;
|
||||
this.autoCancel =
|
||||
(res.data.result.autoCancel - new Date().getTime()) / 1000;
|
||||
}
|
||||
else if(res.data.code == 32000){
|
||||
setTimeout(()=>{
|
||||
uni.redirectTo({
|
||||
url: `/pages/order/myOrder?status=0`
|
||||
});
|
||||
},500)
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
API_Trade.getCashierData(parms).then((res) => {
|
||||
if (res.data.success) {
|
||||
cashierParams.value = res.data.result
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
payList.value = res.data.result.support.filter((item: string) => item != 'ALIPAY')
|
||||
// #endif
|
||||
|
||||
awaitPay(payment){
|
||||
this.$u.throttle(()=>{
|
||||
this.pay(payment)
|
||||
}, 2000)
|
||||
},
|
||||
if (routerVal.value.recharge_sn) {
|
||||
payList.value = res.data.result.support.filter((item: string) => item != 'WALLET')
|
||||
} else {
|
||||
payList.value = res.data.result.support
|
||||
}
|
||||
|
||||
//订单支付
|
||||
async pay(payment) {
|
||||
|
||||
// 支付编号
|
||||
const sn = this.sn;
|
||||
// 交易类型【交易号|订单号】
|
||||
const orderType = this.orderType;
|
||||
// #ifdef H5
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
|
||||
payList.value = res.data.result.support.filter((item: string) => item != 'ALIPAY')
|
||||
if (orderType.value == 'RECHARGE') {
|
||||
payList.value = res.data.result.support.filter((item: string) => item == 'WECHAT')
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
const clientType = this.paymentType;
|
||||
let params = {
|
||||
sn,
|
||||
orderType,
|
||||
clientType,
|
||||
};
|
||||
walletValue.value = res.data.result.walletValue
|
||||
const cancelAt = Number(res.data.result.autoCancel)
|
||||
autoCancelTime.value = cancelAt > 0 ? Math.max(cancelAt - Date.now(), 0) : 0
|
||||
} else if (res.data.code == 32000) {
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/order/myOrder?status=0' })
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//支付方式 WECHAT/ALIPAY
|
||||
const paymentMethod = payment;
|
||||
// 客户端类型 APP/NATIVE/JSAPI/H5
|
||||
const paymentClient = this.paymentClient;
|
||||
|
||||
uni.showLoading({
|
||||
title: "正在唤起支付...",
|
||||
mask:true
|
||||
});
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
//APP pay
|
||||
// 初始化支付签名
|
||||
await API_Trade.initiatePay(paymentMethod, paymentClient, params).then(
|
||||
(signXml) => {
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
//如果支付异常
|
||||
if (!signXml.data.success) {
|
||||
uni.showToast({
|
||||
title: signXml.data.message,
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let payForm = signXml.data.result;
|
||||
|
||||
let paymentType = paymentMethod === "WECHAT" ? "wxpay" : "alipay";
|
||||
|
||||
if(paymentMethod === "WALLET"){
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
this.callback(paymentMethod)
|
||||
}
|
||||
else{
|
||||
uni.requestPayment({
|
||||
provider: paymentType,
|
||||
orderInfo: payForm || '',
|
||||
success: (e) => {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
this.callback(paymentMethod)
|
||||
},
|
||||
fail: (e) => {
|
||||
console.log(this);
|
||||
this.exception = e;
|
||||
uni.showModal({
|
||||
content: "支付失败,如果您已支付,请勿反复支付",
|
||||
showCancel: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
//APP pay
|
||||
// #endif
|
||||
function awaitPay(payment: string) {
|
||||
proxy.$u.throttle(() => {
|
||||
pay(payment)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
//#ifdef H5
|
||||
//H5 pay
|
||||
await API_Trade.initiatePay(paymentMethod, paymentClient, params).then(
|
||||
(res) => {
|
||||
let response = res.data;
|
||||
//如果非支付宝支付才需要进行判定,因为支付宝h5支付是直接输出的,没有返回所谓的消息状态
|
||||
if(paymentMethod !== "ALIPAY"){
|
||||
//如果支付异常
|
||||
if (!response.success) {
|
||||
uni.showToast({
|
||||
title: response.message,
|
||||
duration: 2000,
|
||||
icon:"none"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (paymentMethod === "ALIPAY") {
|
||||
document.write(response);
|
||||
} else if (paymentMethod === "WECHAT") {
|
||||
if (this.isWeiXin()) {
|
||||
//微信公众号支付
|
||||
WeixinJSBridge.invoke(
|
||||
"getBrandWCPayRequest",
|
||||
response.result,
|
||||
(res) => {
|
||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||
// 使用以上方式判断前端返回,微信团队郑重提示:
|
||||
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
this.callback(paymentMethod)
|
||||
|
||||
} else {
|
||||
uni.showModal({
|
||||
content: "支付失败,如果您已支付,请勿反复支付",
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
} else {
|
||||
window.location.href = JSON.parse(response.result).h5_url;
|
||||
const searchParams = {
|
||||
...params,
|
||||
price:this.cashierParams,
|
||||
}
|
||||
const timer = setInterval(()=>{
|
||||
payCallback(searchParams).then(res=>{
|
||||
if(res.data.result){
|
||||
clearTimeout(timer);
|
||||
uni.navigateTo({
|
||||
url:"/pages/order/myOrder"
|
||||
})
|
||||
}
|
||||
})
|
||||
},3000)
|
||||
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
}
|
||||
} else if (paymentMethod === "WALLET") {
|
||||
uni.showToast({
|
||||
title: response.message,
|
||||
icon: "none",
|
||||
});
|
||||
if (response.success) {
|
||||
this.callback(paymentMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//H5pay
|
||||
// #endif
|
||||
function padTime(val: number) {
|
||||
return String(val != null ? val : 0).padStart(2, '0')
|
||||
}
|
||||
|
||||
//#ifdef MP-WEIXIN
|
||||
//微信小程序
|
||||
await API_Trade.initiatePay(paymentMethod, paymentClient, params).then(
|
||||
(res) => {
|
||||
let response = res.data.result;
|
||||
//如果支付异常
|
||||
if (!res.data.success) {
|
||||
uni.showModal({
|
||||
content: res.data.message,
|
||||
showCancel: false,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (paymentMethod === "WECHAT") {
|
||||
uni.requestPayment({
|
||||
provider: "wxpay",
|
||||
appid: response.appid,
|
||||
timeStamp: response.timeStamp,
|
||||
nonceStr: response.nonceStr,
|
||||
package: response.package,
|
||||
signType: response.signType,
|
||||
paySign: response.paySign,
|
||||
success: (e) => {
|
||||
console.log(e);
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
this.callback(paymentMethod)
|
||||
|
||||
},
|
||||
fail: (e) => {
|
||||
console.log(e);
|
||||
this.exception = e;
|
||||
uni.showModal({
|
||||
content: "支付失败,如果您已支付,请勿反复支付",
|
||||
showCancel: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "支付成功!",
|
||||
});
|
||||
this.callback(paymentMethod)
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
// #endif
|
||||
},
|
||||
isWeiXin() {
|
||||
var ua = window.navigator.userAgent.toLowerCase();
|
||||
if (ua.match(/MicroMessenger/i) == "micromessenger") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
function onPayTimeout() {
|
||||
uni.showToast({ title: '支付超时,请重新下单', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/order/myOrder?status=0' })
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function goPayError(message = '支付失败,如果您已支付,请勿反复支付') {
|
||||
const query = [`orderSn=${encodeURIComponent(sn.value)}`, `message=${encodeURIComponent(message)}`]
|
||||
uni.navigateTo({ url: `/pages/cart/payment/error?${query.join('&')}` })
|
||||
}
|
||||
|
||||
async function pay(payment: string) {
|
||||
const params = {
|
||||
sn: sn.value,
|
||||
orderType: orderType.value,
|
||||
clientType: paymentType.value,
|
||||
}
|
||||
const paymentMethod = payment
|
||||
const client = paymentClient.value
|
||||
|
||||
uni.showLoading({ title: '正在唤起支付...', mask: true })
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
await API_Trade.initiatePay(paymentMethod, client, params).then((signXml) => {
|
||||
hideLoadingIfNeeded()
|
||||
if (!signXml.data.success) {
|
||||
uni.showToast({ title: signXml.data.message, duration: 2000 })
|
||||
return
|
||||
}
|
||||
const payForm = signXml.data.result
|
||||
const provider = paymentMethod === 'WECHAT' ? 'wxpay' : 'alipay'
|
||||
if (paymentMethod === 'WALLET') {
|
||||
uni.showToast({ icon: 'none', title: '支付成功!' })
|
||||
callback(paymentMethod)
|
||||
} else {
|
||||
uni.requestPayment({
|
||||
provider,
|
||||
orderInfo: payForm || '',
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: '支付成功!' })
|
||||
callback(paymentMethod)
|
||||
},
|
||||
fail: () => {
|
||||
goPayError()
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
await API_Trade.initiatePay(paymentMethod, client, params).then((res) => {
|
||||
const response = res.data
|
||||
if (paymentMethod !== 'ALIPAY' && !response.success) {
|
||||
uni.showToast({ title: response.message, duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (paymentMethod === 'ALIPAY') {
|
||||
document.write(response)
|
||||
} else if (paymentMethod === 'WECHAT') {
|
||||
if (isWeiXin()) {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', response.result, (payRes: any) => {
|
||||
if (payRes.err_msg == 'get_brand_wcpay_request:ok') {
|
||||
uni.showToast({ icon: 'none', title: '支付成功!' })
|
||||
callback(paymentMethod)
|
||||
} else {
|
||||
goPayError()
|
||||
}
|
||||
})
|
||||
hideLoadingIfNeeded()
|
||||
} else {
|
||||
window.location.href = JSON.parse(response.result).h5_url
|
||||
const searchParams = { ...params, price: cashierParams.value }
|
||||
const timer = setInterval(() => {
|
||||
payCallback(searchParams).then((cbRes) => {
|
||||
if (cbRes.data.result) {
|
||||
clearInterval(timer)
|
||||
uni.navigateTo({ url: '/pages/order/myOrder' })
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
hideLoadingIfNeeded()
|
||||
}
|
||||
} else if (paymentMethod === 'WALLET') {
|
||||
uni.showToast({ title: response.message, icon: 'none' })
|
||||
if (response.success) callback(paymentMethod)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
await API_Trade.initiatePay(paymentMethod, client, params).then((res) => {
|
||||
const response = res.data.result
|
||||
if (!res.data.success) {
|
||||
uni.showModal({ content: res.data.message, showCancel: false })
|
||||
return
|
||||
}
|
||||
if (paymentMethod === 'WECHAT') {
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
appid: response.appid,
|
||||
timeStamp: response.timeStamp,
|
||||
nonceStr: response.nonceStr,
|
||||
package: response.package,
|
||||
signType: response.signType,
|
||||
paySign: response.paySign,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: '支付成功!' })
|
||||
callback(paymentMethod)
|
||||
},
|
||||
fail: () => {
|
||||
goPayError()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ icon: 'none', title: '支付成功!' })
|
||||
callback(paymentMethod)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
function isWeiXin() {
|
||||
const ua = window.navigator.userAgent.toLowerCase()
|
||||
return ua.match(/MicroMessenger/i) == 'micromessenger'
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.method_icon {
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.method-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.method_name {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/deep/ .u-row {
|
||||
width: 100% !important;
|
||||
display: flex;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.method_name,
|
||||
.col1 {
|
||||
color: #333;
|
||||
padding-left: 20rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col1 {
|
||||
text-align: center;
|
||||
flex: 99;
|
||||
}
|
||||
|
||||
.col3 {
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.payItem {
|
||||
padding: 13px 25rpx;
|
||||
border-top: 1px solid #f9f9f9;
|
||||
|
||||
line-height: 100rpx;
|
||||
font-size: 36rpx;
|
||||
padding: 0 30rpx;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.payItem-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
padding-top: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.payItem-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 100rpx;
|
||||
padding-top: 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.ptips {
|
||||
font-size: 32rpx;
|
||||
margin: 20rpx 0;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
> span {
|
||||
.ptips-countdown {
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.ptips-price {
|
||||
.price-text {
|
||||
font-size: 40rpx;
|
||||
color: #df5a52;
|
||||
color: $main-color;
|
||||
font-weight: 600;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown-box {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.countdown-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44rpx;
|
||||
height: 44rpx;
|
||||
padding: 0 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #008ffa;
|
||||
border: 1rpx solid #008ffa;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.countdown-colon {
|
||||
margin: 0 6rpx;
|
||||
font-size: 28rpx;
|
||||
color: #008ffa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 392rpx !important;
|
||||
height: 296rpx !important;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<view class="wrapper" v-if="flag">
|
||||
<view :style="themeStyle" class="wrapper" v-if="flag">
|
||||
<div class='goods' v-if="selectedGoods">
|
||||
<image class="goods-image" :src="selectedGoods.thumbnail" alt="">
|
||||
<p class="goodsName">{{selectedGoods.goodsName}}</p>
|
||||
<div class="goodsPrice">{{(selectedGoods.promotionPrice || selectedGoods.price ) | unitPrice('¥')}}</div>
|
||||
<image class="goods-image" :src="selectedGoods.thumbnail" alt="" />
|
||||
<p class="goodsName">{{selectedGoods.goodsName}}</p>
|
||||
<div class="goodsPrice">{{ unitPrice(selectedGoods.promotionPrice || selectedGoods.price, '¥') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tips">
|
||||
@@ -14,6 +14,9 @@
|
||||
<span v-if="isBuy &&!master.toBeGroupedNum >0">
|
||||
已成功拼团
|
||||
</span>
|
||||
<span v-if="!master.toBeGroupedNum >0">
|
||||
拼团已结束
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isMaster && !isOver">
|
||||
@@ -24,7 +27,7 @@
|
||||
去首页逛逛
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMaster && !isOver && !isBuy">
|
||||
<div v-if="!isMaster && !isOver && !isBuy && master.toBeGroupedNum">
|
||||
<div class="share-user" @click="toBuy">
|
||||
参与拼团
|
||||
</div>
|
||||
@@ -45,184 +48,203 @@
|
||||
</div>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<div class="count-down" v-if="!isOver">
|
||||
<u-count-down bg-color="#ededed" :hide-zero-day="true" @end="isOver" :timestamp="timeStamp"></u-count-down>
|
||||
<div class="count-down" v-if="!isOver && master.toBeGroupedNum">
|
||||
<u-count-down
|
||||
v-if="timeStamp > 0"
|
||||
bgColor="#ededed"
|
||||
:time="timeStamp * 1000"
|
||||
format="HH:mm:ss"
|
||||
@finish="onCountDownEnd"
|
||||
></u-count-down>
|
||||
</div>
|
||||
|
||||
<div class="user-list" v-if="data.pintuanMemberVOS">
|
||||
<div class="user-item" v-for="(item,index) in data.pintuanMemberVOS" :key="index">
|
||||
<span class="master" v-if="item.orderSn == ''">团长</span>
|
||||
<image class="img" :src="item.face" alt="">
|
||||
<image class="img" :src="item.face" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<popupGoods :addr="addr" ref="popupGoods" :buyMask="maskFlag" @closeBuy="closePopupBuy" :goodsDetail="goodsDetail" :goodsSpec="goodsSpec" v-if="goodsDetail.id " @handleClickSku="getGoodsDetail" />
|
||||
<shares @close="closeShare" :link="'/pages/cart/payment/shareOrderGoods?sn='+this.routers.sn+'&sku='+this.routers.sku+'&goodsId='+this.routers.goodsId" type="pintuan"
|
||||
:thumbnail="data.promotionGoods.thumbnail" :goodsName="data.promotionGoods.goodsName" v-if="shareFlag " />
|
||||
<popupGoods
|
||||
:addr="addr"
|
||||
ref="popupGoodsRef"
|
||||
:buyMask="maskFlag"
|
||||
@closeBuy="closePopupBuy"
|
||||
:goodsDetail="goodsDetail"
|
||||
:goodsSpec="goodsSpec"
|
||||
v-if="goodsDetail.id"
|
||||
@handleClickSku="getGoodsDetail"
|
||||
/>
|
||||
<shares
|
||||
@close="closeShare"
|
||||
:link="shareLink"
|
||||
type="pintuan"
|
||||
:thumbnail="data.promotionGoods.thumbnail"
|
||||
:goodsName="data.promotionGoods.goodsName"
|
||||
v-if="shareFlag"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGoods } from "@/api/goods.js";
|
||||
import { getPinTuanShare } from "@/api/order";
|
||||
import shares from "@/components/m-share/index";
|
||||
import storage from "@/utils/storage.js";
|
||||
import popupGoods from "@/components/m-buy/goods"; //购物车商品的模块
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getGoods } from '@/api/goods.js'
|
||||
import { getPinTuanShare } from '@/api/order'
|
||||
import shares from '@/components/m-share/index'
|
||||
import storage from '@/utils/storage.js'
|
||||
import popupGoods from '@/components/m-buy/goods'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
flag: false, //判断接口是否正常请求
|
||||
addr: {
|
||||
id: "",
|
||||
},
|
||||
maskFlag: false, //商品弹框
|
||||
timeStamp: 0,
|
||||
shareFlag: false,
|
||||
data: "",
|
||||
isMaster: true,
|
||||
selectedGoods: "", //选择的商品规格昵称
|
||||
routers: "", //传参数据
|
||||
goodsDetail: "", //商品详情
|
||||
goodsSpec: "",
|
||||
master: "", // 团长
|
||||
PromotionList: "", //优惠集合
|
||||
isGroup: false, //是否拼团
|
||||
isOver: false, //是否结束活动
|
||||
isBuy: false, //当前用户是是否购买
|
||||
};
|
||||
},
|
||||
components: {
|
||||
shares,
|
||||
popupGoods,
|
||||
},
|
||||
watch: {
|
||||
isGroup(val) {
|
||||
if (val) {
|
||||
let timer = setInterval(() => {
|
||||
this.$refs.popupGoods.buyType = "PINTUAN";
|
||||
clearInterval(timer);
|
||||
}, 100);
|
||||
} else {
|
||||
this.$refs.popupGoods.buyType = "";
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const popupGoodsRef = ref<any>(null)
|
||||
const flag = ref(false)
|
||||
const addr = ref({ id: '' })
|
||||
const maskFlag = ref(false)
|
||||
const timeStamp = ref(0)
|
||||
const shareFlag = ref(false)
|
||||
const data = ref<any>(null)
|
||||
const isMaster = ref(true)
|
||||
const selectedGoods = ref<any>(null)
|
||||
const routers = ref<Record<string, string>>({})
|
||||
const goodsDetail = ref<any>({})
|
||||
const goodsSpec = ref<any>(null)
|
||||
const master = ref<any>(null)
|
||||
const PromotionList = ref<any>(null)
|
||||
const isGroup = ref(false)
|
||||
const isOver = ref(false)
|
||||
const isBuy = ref(false)
|
||||
|
||||
const shareLink = computed(() => {
|
||||
const { sn, sku, goodsId } = routers.value
|
||||
return `/pages/cart/payment/shareOrderGoods?sn=${sn}&sku=${sku}&goodsId=${goodsId}`
|
||||
})
|
||||
|
||||
watch(isGroup, (val) => {
|
||||
if (val) {
|
||||
const timer = setInterval(() => {
|
||||
if (popupGoodsRef.value) {
|
||||
popupGoodsRef.value.buyType = 'PINTUAN'
|
||||
}
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.routers = options;
|
||||
},
|
||||
mounted() {
|
||||
this.init(this.routers.sn, this.routers.sku);
|
||||
},
|
||||
methods: {
|
||||
closeShare() {
|
||||
this.shareFlag = false;
|
||||
},
|
||||
// 这里的话得先跳到商品详情才能购买商品
|
||||
toBuy() {
|
||||
this.maskFlag = true;
|
||||
this.$refs.popupGoods.parentOrder = {
|
||||
...this.master,
|
||||
orderSn: this.routers.sn,
|
||||
};
|
||||
this.$refs.popupGoods.isMask = true;
|
||||
this.$refs.popupGoods.isClose = true;
|
||||
this.$refs.popupGoods.buyType = "PINTUAN";
|
||||
},
|
||||
// 分享
|
||||
share() {
|
||||
this.shareFlag = true;
|
||||
},
|
||||
closePopupBuy(val) {
|
||||
this.maskFlag = false;
|
||||
},
|
||||
// 实例化本页面
|
||||
async init(sn, sku) {
|
||||
let res = await getPinTuanShare(sn, sku);
|
||||
if (res.data.success && res.data.result.promotionGoods) {
|
||||
this.flag = true;
|
||||
this.data = res.data.result;
|
||||
this.selectedGoods = res.data.result.promotionGoods;
|
||||
let endTime = Date.parse(
|
||||
res.data.result.promotionGoods.endTime.replace(/-/g, "/")
|
||||
);
|
||||
// 获取当前剩余的拼团商品时间
|
||||
let timeStamp = Date.parse(new Date(endTime)) / 1000;
|
||||
clearInterval(timer)
|
||||
}, 100)
|
||||
} else if (popupGoodsRef.value) {
|
||||
popupGoodsRef.value.buyType = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前时间时间戳
|
||||
let dateTime = Date.parse(new Date()) / 1000;
|
||||
onLoad((options) => {
|
||||
routers.value = options || {}
|
||||
})
|
||||
|
||||
this.timeStamp = parseInt(timeStamp - dateTime);
|
||||
onMounted(() => {
|
||||
init(routers.value.sn, routers.value.sku)
|
||||
})
|
||||
|
||||
this.timeStamp <= 0 ? (this.isOver = true) : (this.isOver = false);
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
// 获取剩余拼团人数
|
||||
this.master =
|
||||
res.data.result.pintuanMemberVOS.length != 0 &&
|
||||
res.data.result.pintuanMemberVOS.filter((item) => {
|
||||
return item.orderSn == "";
|
||||
})[0];
|
||||
function closeShare() {
|
||||
shareFlag.value = false
|
||||
}
|
||||
|
||||
// 获取当前是否是拼团本人
|
||||
if (
|
||||
storage.getUserInfo(this.routers.sku, this.routers.goodsId).id ==
|
||||
this.master.memberId
|
||||
) {
|
||||
this.isMaster = true;
|
||||
} else {
|
||||
this.isMaster = false;
|
||||
// 获取商品详情
|
||||
this.getGoodsDetail({
|
||||
id: this.routers.sku,
|
||||
goodsId: this.routers.goodsId,
|
||||
});
|
||||
function onCountDownEnd() {
|
||||
isOver.value = true
|
||||
}
|
||||
|
||||
function toBuy() {
|
||||
maskFlag.value = true
|
||||
if (!popupGoodsRef.value) return
|
||||
popupGoodsRef.value.parentOrder = {
|
||||
...master.value,
|
||||
orderSn: routers.value.sn,
|
||||
}
|
||||
popupGoodsRef.value.isMask = true
|
||||
popupGoodsRef.value.isClose = true
|
||||
popupGoodsRef.value.buyType = 'PINTUAN'
|
||||
}
|
||||
|
||||
function share() {
|
||||
shareFlag.value = true
|
||||
}
|
||||
|
||||
function closePopupBuy() {
|
||||
maskFlag.value = false
|
||||
}
|
||||
|
||||
async function init(sn: string, sku: string) {
|
||||
const res = await getPinTuanShare(sn, sku)
|
||||
if (res.data.success && res.data.result.promotionGoods) {
|
||||
flag.value = true
|
||||
data.value = res.data.result
|
||||
selectedGoods.value = res.data.result.promotionGoods
|
||||
const endTime = Date.parse(
|
||||
res.data.result.promotionGoods.endTime.replace(/-/g, '/')
|
||||
)
|
||||
const endTimestamp = Date.parse(new Date(endTime) as any) / 1000
|
||||
const dateTime = Date.parse(new Date() as any) / 1000
|
||||
timeStamp.value = parseInt(String(endTimestamp - dateTime))
|
||||
isOver.value = timeStamp.value <= 0
|
||||
|
||||
master.value =
|
||||
res.data.result.pintuanMemberVOS.length != 0 &&
|
||||
res.data.result.pintuanMemberVOS.filter((item: any) => item.orderSn == '')[0]
|
||||
|
||||
if (
|
||||
storage.getUserInfo(routers.value.sku, routers.value.goodsId).id ==
|
||||
master.value.memberId
|
||||
) {
|
||||
isMaster.value = true
|
||||
} else {
|
||||
isMaster.value = false
|
||||
getGoodsDetail({
|
||||
id: routers.value.sku,
|
||||
goodsId: routers.value.goodsId,
|
||||
})
|
||||
}
|
||||
|
||||
if (storage.getUserInfo().id) {
|
||||
const bought = res.data.result.pintuanMemberVOS.filter(
|
||||
(item: any) => item.memberId == storage.getUserInfo().id
|
||||
)
|
||||
isBuy.value = bought.length != 0
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '当前拼团单有误!请联系管理员重试',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getGoodsDetail(val: { id: string; goodsId: string }) {
|
||||
const { id, goodsId } = val
|
||||
uni.showLoading({ title: '加载中', mask: true })
|
||||
getGoods(id, goodsId).then((response) => {
|
||||
goodsDetail.value = response.data.result.data
|
||||
selectedGoods.value = response.data.result.data
|
||||
goodsSpec.value = response.data.result.specs
|
||||
hideLoadingIfNeeded()
|
||||
PromotionList.value = response.data.result.promotionMap
|
||||
if (PromotionList.value) {
|
||||
Object.keys(PromotionList.value).forEach((item) => {
|
||||
if (item.indexOf('PINTUAN') == 0) {
|
||||
isGroup.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前商品是否已经购买
|
||||
if (storage.getUserInfo().id) {
|
||||
let isBuy = res.data.result.pintuanMemberVOS.filter((item) => {
|
||||
return item.memberId == storage.getUserInfo().id;
|
||||
});
|
||||
isBuy.length != 0 ? (this.isBuy = true) : (this.isBuy = false);
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "当前拼团单有误!请联系管理员重试",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
// 获取商品详情
|
||||
getGoodsDetail(val) {
|
||||
let { id, goodsId } = val;
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
mask: true,
|
||||
});
|
||||
getGoods(id, goodsId).then((response) => {
|
||||
this.goodsDetail = response.data.result.data;
|
||||
this.selectedGoods = response.data.result.data;
|
||||
this.goodsSpec = response.data.result.specs;
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
this.PromotionList = response.data.result.promotionMap;
|
||||
|
||||
// 判断是否拼团活动 如果有则显示拼团活动信息
|
||||
this.PromotionList &&
|
||||
Object.keys(this.PromotionList).forEach((item) => {
|
||||
if (item.indexOf("PINTUAN") == 0) {
|
||||
this.isGroup = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
handleClickHome() {
|
||||
uni.switchTab({
|
||||
url: "/pages/tabbar/home/index",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
function handleClickHome() {
|
||||
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -318,7 +340,7 @@ page {
|
||||
background: $main-color;
|
||||
}
|
||||
.disabled {
|
||||
background: rgba($main-color, $alpha: 0.2);
|
||||
background: var(--theme-primary-20, rgba(255, 60, 42, 0.2));
|
||||
}
|
||||
.home {
|
||||
color: $main-color;
|
||||
|
||||
@@ -1,168 +1,188 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="pay-wrapper">
|
||||
<div class="pay-money">
|
||||
¥{{ Number(payPrice) | unitPrice }}
|
||||
</div>
|
||||
<div class="pay-btns">
|
||||
<div v-show="!from" @click="checkOrder">查看{{ this.orderType == "RECHARGE" ? '余额' : '订单' }}</div>
|
||||
<div @click="navigateTo('/pages/tabbar/home/index', 'switch')">回到首页</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-box">
|
||||
<div class="pay-tag-box">
|
||||
<h2>订单支付成功!</h2>
|
||||
<div class="pay-item">
|
||||
<div>
|
||||
支付方式:
|
||||
</div>
|
||||
<div>{{ paymentMethod | paymentTypeFilter }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<goodsRecommend />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import goodsRecommend from "@/components/m-goods-recommend";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
checked: false,
|
||||
paymentMethod: "",
|
||||
|
||||
from: "",
|
||||
payPrice: 0,
|
||||
goodsList: [],
|
||||
activeColor: this.$mainColor,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
goodsRecommend,
|
||||
},
|
||||
filters: {
|
||||
paymentTypeFilter(val) {
|
||||
switch (val) {
|
||||
case "WECHAT":
|
||||
return "微信";
|
||||
case "ALIPAY":
|
||||
return "支付宝";
|
||||
case "WALLET":
|
||||
return "余额支付";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.paymentMethod = options.paymentMethod || "";
|
||||
this.from = options.from || "";
|
||||
this.payPrice = options.payPrice || 0;
|
||||
this.orderType = options.orderType;
|
||||
|
||||
},
|
||||
methods: {
|
||||
checkOrder() {
|
||||
/**
|
||||
* 查看订单
|
||||
* 1.充值跳转到明细里面
|
||||
* 2.支付跳转到订单详情
|
||||
*/
|
||||
if (this.orderType == "RECHARGE") {
|
||||
uni.reLaunch({
|
||||
url: `/pages/mine/deposit/operation`,
|
||||
});
|
||||
} else {
|
||||
this.navigateTo("/pages/order/myOrder?status=0");
|
||||
}
|
||||
},
|
||||
|
||||
navigateTo(url, type) {
|
||||
if (type === "switch") {
|
||||
uni.switchTab({
|
||||
url,
|
||||
});
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.subscribe {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 auto 40rpx auto;
|
||||
padding: 0 20rpx 20rpx;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.pay-btns {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
|
||||
>div {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-money {
|
||||
line-height: 1;
|
||||
font-size: 50rpx;
|
||||
color: #fff;
|
||||
margin-bottom: 100rpx;
|
||||
}
|
||||
|
||||
.pay-item {
|
||||
font-weight: bold;
|
||||
margin: 32rpx 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
color: rgba($color: $main-color, $alpha: 0.8);
|
||||
}
|
||||
|
||||
.pay-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pay-tag-box {
|
||||
width: 80%;
|
||||
margin: 80rpx auto 40rpx auto;
|
||||
padding: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba($color: $main-color, $alpha: 0.2);
|
||||
|
||||
>h2 {
|
||||
margin-top: 20rpx;
|
||||
font-size: 40rpx;
|
||||
color: $main-color;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-wrapper {
|
||||
background-image: linear-gradient(90deg, #fa123b, #ff6b35, #ff9f28, #ffcc03);
|
||||
height: 480rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pay-box {
|
||||
transform: translateY(-100rpx);
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-top-right-radius: 100rpx;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div :style="themeStyle" class="wrapper">
|
||||
<div class="pay-wrapper">
|
||||
<div class="pay-money">
|
||||
¥{{ unitPrice(Number(payPrice)) }}
|
||||
</div>
|
||||
<div class="pay-btns">
|
||||
<div v-show="!from" @click="checkOrder">查看{{ orderType == "RECHARGE" ? '余额' : '订单' }}</div>
|
||||
<div @click="navigateTo('/pages/tabbar/home/index', 'switch')">回到首页</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-box">
|
||||
<div class="pay-tag-box">
|
||||
<h2>订单支付成功!</h2>
|
||||
<div class="pay-item">
|
||||
<div>
|
||||
支付方式:
|
||||
</div>
|
||||
<div>{{ paymentTypeFilter(paymentMethod) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<goodsRecommend />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import goodsRecommend from '@/components/m-goods-recommend'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import {
|
||||
prefetchMpMessageTemplates,
|
||||
subscribePayMessageThen,
|
||||
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const paymentMethod = ref('')
|
||||
const from = ref('')
|
||||
const payPrice = ref<number | string>(0)
|
||||
const orderType = ref('')
|
||||
const orderPromotionType = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
paymentMethod.value = options.paymentMethod || ''
|
||||
from.value = options.from || ''
|
||||
payPrice.value = options.payPrice || 0
|
||||
orderType.value = options.orderType || ''
|
||||
orderPromotionType.value = options.orderPromotionType || ''
|
||||
// #ifdef MP-WEIXIN
|
||||
prefetchMpMessageTemplates()
|
||||
// #endif
|
||||
})
|
||||
|
||||
function paymentTypeFilter(val: string) {
|
||||
switch (val) {
|
||||
case 'WECHAT':
|
||||
return '微信'
|
||||
case 'ALIPAY':
|
||||
return '支付宝'
|
||||
case 'WALLET':
|
||||
return '余额支付'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function checkOrder() {
|
||||
if (orderType.value == 'RECHARGE') {
|
||||
uni.reLaunch({
|
||||
url: '/pages/mine/deposit/operation',
|
||||
})
|
||||
return
|
||||
}
|
||||
const doGoOrder = () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/order/myOrder?status=0',
|
||||
})
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
subscribePayMessageThen(doGoOrder, {
|
||||
orderPromotionType: orderPromotionType.value,
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
doGoOrder()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function navigateTo(url: string, type?: string) {
|
||||
const doNavigate = () => {
|
||||
if (type === 'switch') {
|
||||
uni.switchTab({ url })
|
||||
} else {
|
||||
uni.redirectTo({ url })
|
||||
}
|
||||
}
|
||||
if (orderType.value == 'RECHARGE') {
|
||||
doNavigate()
|
||||
return
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
subscribePayMessageThen(doNavigate, {
|
||||
orderPromotionType: orderPromotionType.value,
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
doNavigate()
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.subscribe {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 auto 40rpx auto;
|
||||
padding: 0 20rpx 20rpx;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.pay-btns {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
|
||||
>div {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-money {
|
||||
line-height: 1;
|
||||
font-size: 50rpx;
|
||||
color: #fff;
|
||||
margin-bottom: 100rpx;
|
||||
}
|
||||
|
||||
.pay-item {
|
||||
font-weight: bold;
|
||||
margin: 32rpx 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
color: var(--theme-primary-70, rgba(255, 60, 42, 0.7));
|
||||
}
|
||||
|
||||
.pay-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pay-tag-box {
|
||||
width: 80%;
|
||||
margin: 80rpx auto 40rpx auto;
|
||||
padding: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: var(--theme-primary-20, rgba(255, 60, 42, 0.2));
|
||||
|
||||
>h2 {
|
||||
margin-top: 20rpx;
|
||||
font-size: 40rpx;
|
||||
color: $main-color;
|
||||
}
|
||||
}
|
||||
|
||||
.pay-wrapper {
|
||||
background-image: linear-gradient(90deg, $main-color, $light-color, $aider-light-color);
|
||||
height: 480rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pay-box {
|
||||
transform: translateY(-100rpx);
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-top-right-radius: 100rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,21 +2,16 @@
|
||||
<default-page v-if="type" :type="type" title="您的设备已断网" :isBtn="false" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import defaultPage from '@/components/default-page/default-page.vue';
|
||||
export default {
|
||||
components: {
|
||||
defaultPage
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: undefined
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
|
||||
}
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import defaultPage from '@/components/default-page/default-page.vue'
|
||||
|
||||
const type = ref<string>()
|
||||
|
||||
onLoad(() => {
|
||||
type.value = 'msg'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,356 +1,433 @@
|
||||
<template>
|
||||
<view class="add-address">
|
||||
<div class="uForm">
|
||||
<u-form :border-bottom="false" :model="form" ref="uForm" :error-type="['toast']" :rule="rules">
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="selectAddress" @click="clickUniMap">
|
||||
选择收货地址
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<u-form-item class="border" label="收货人" label-width="130" prop="name">
|
||||
<u-input v-model="form.name" clearable placeholder="请输入收货人姓名" />
|
||||
</u-form-item>
|
||||
|
||||
<u-form-item label="手机号码" label-width="130" prop="mobile">
|
||||
<u-input v-model="form.mobile" type="number" maxlength="11" placeholder="请输入收货人手机号码" />
|
||||
</u-form-item>
|
||||
<u-form-item label="所在区域" label-width="130" prop="___path">
|
||||
<div @click="showPicker" >
|
||||
{{ form.___path || '请选择所在地区' }}
|
||||
</div>
|
||||
</u-form-item>
|
||||
<u-form-item class="detailAddress" label="详细地址" label-width="130" prop="detail">
|
||||
<u-input type="textarea" v-model="form.detail" maxlength="100" height="150" placeholder="街道楼牌号等" />
|
||||
</u-form-item>
|
||||
<u-form-item label="地址别名" label-width="130">
|
||||
<u-input v-model="form.alias" placeholder="请输入地址别名" />
|
||||
</u-form-item>
|
||||
<u-checkbox-group shape="circle" size="30">
|
||||
<u-checkbox :active-color="lightColor" v-model="form.isDefault">设为默认地址</u-checkbox>
|
||||
</u-checkbox-group>
|
||||
|
||||
<div class="saveBtn" @click="save">保存</div>
|
||||
</u-form>
|
||||
|
||||
<m-city :provinceData="list" headTitle="区域选择" ref="cityPicker" @funcValue="getpickerParentValue" pickerSize="4">
|
||||
</m-city>
|
||||
|
||||
<uniMap v-if="mapFlag" @close="closeMap" @callback="callBackAddress" />
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { addAddress, editAddress, getAddressDetail } from "@/api/address.js";
|
||||
import city from "@/components/m-city/m-city.vue";
|
||||
import uniMap from "@/components/uniMap";
|
||||
import permision from "@/js_sdk/wa-permission/permission.js";
|
||||
export default {
|
||||
components: {
|
||||
"m-city": city,
|
||||
uniMap,
|
||||
},
|
||||
onShow() {
|
||||
// 判断当前系统权限定位是否开启
|
||||
},
|
||||
methods: {
|
||||
// 关闭地图
|
||||
closeMap() {
|
||||
this.mapFlag = false;
|
||||
},
|
||||
// 打开地图并访问权限
|
||||
clickUniMap() {
|
||||
// #ifdef APP-PLUS
|
||||
if (plus.os.name == "iOS") {
|
||||
// ios系统
|
||||
permision.judgeIosPermission("location")
|
||||
? (this.mapFlag = true)
|
||||
: this.refuseMap();
|
||||
} else {
|
||||
// 安卓
|
||||
this.requestAndroidPermission(
|
||||
"android.permission.ACCESS_FINE_LOCATION"
|
||||
);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
this.mapFlag = true;
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 如果拒绝权限 提示区设置
|
||||
refuseMap() {
|
||||
uni.showModal({
|
||||
title: "温馨提示",
|
||||
content: "您已拒绝定位,请开启",
|
||||
confirmText: "去设置",
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
//打开授权设置
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.getSystemInfo({
|
||||
success(res) {
|
||||
if (res.platform == "ios") {
|
||||
//IOS
|
||||
plus.runtime.openURL("app-settings://");
|
||||
} else if (res.platform == "android") {
|
||||
//安卓
|
||||
let main = plus.android.runtimeMainActivity();
|
||||
let Intent = plus.android.importClass(
|
||||
"android.content.Intent"
|
||||
);
|
||||
let mIntent = new Intent("android.settings.ACTION_SETTINGS");
|
||||
main.startActivity(mIntent);
|
||||
}
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 获取安卓是否拥有地址权限
|
||||
async requestAndroidPermission(permisionID) {
|
||||
var result = await permision.requestAndroidPermission(permisionID);
|
||||
|
||||
if (result == 1) {
|
||||
this.mapFlag = true;
|
||||
} else {
|
||||
this.refuseMap();
|
||||
}
|
||||
},
|
||||
|
||||
// 选择地址后数据的回调
|
||||
callBackAddress(val) {
|
||||
console.log(val)
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
});
|
||||
|
||||
if (val.regeocode && val) {
|
||||
let address = val.regeocode;
|
||||
this.form.detail = address.formatted_address; //地址详情
|
||||
this.form.___path = val.data.result.name;
|
||||
this.form.consigneeAddressIdPath = val.data.result.id; // 地址id分割
|
||||
this.form.consigneeAddressPath = val.data.result.name; //地址名称, ','分割
|
||||
this.form.lat = val.latitude; //纬度
|
||||
this.form.lon = val.longitude; //经度
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
this.mapFlag = !this.mapFlag; //关闭地图
|
||||
},
|
||||
|
||||
// 保存当前 地址
|
||||
save() {
|
||||
this.$refs.uForm.validate((valid) => {
|
||||
if (valid) {
|
||||
let pages = getCurrentPages(); //获取页面栈
|
||||
let beforePage = pages[pages.length - 2]; //上个页面
|
||||
|
||||
// 如果没有id则为新增地址
|
||||
if (!this.form.id) {
|
||||
// 删除没有的数据
|
||||
delete this.form.___path;
|
||||
addAddress(this.form).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 修改地址
|
||||
delete this.form.___path;
|
||||
delete this.form.updateBy;
|
||||
delete this.form.updateTime;
|
||||
editAddress(this.form).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 三级地址联动回调
|
||||
getpickerParentValue(e) {
|
||||
// 将需要绑定的地址设置为空,并赋值
|
||||
this.form.consigneeAddressIdPath = [];
|
||||
this.form.consigneeAddressPath = [];
|
||||
let name = "";
|
||||
|
||||
e.forEach((item, index) => {
|
||||
if (item.id) {
|
||||
// 遍历数据
|
||||
this.form.consigneeAddressIdPath.push(item.id);
|
||||
this.form.consigneeAddressPath.push(item.localName);
|
||||
name += item.localName;
|
||||
this.form.___path = name;
|
||||
}
|
||||
if (index == e.length - 1) {
|
||||
//如果是最后一个
|
||||
let _town = item.children.filter((_child) => {
|
||||
return _child.id == item.id;
|
||||
});
|
||||
|
||||
this.form.lat = _town[0].center.split(",")[1];
|
||||
this.form.lon = _town[0].center.split(",")[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 显示三级地址联动
|
||||
showPicker() {
|
||||
this.$refs.cityPicker.show();
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
data() {
|
||||
return {
|
||||
lightColor: this.$lightColor, //高亮颜色
|
||||
mapFlag: false, // 地图选择开
|
||||
routerVal: "",
|
||||
form: {
|
||||
detail: "", //地址详情
|
||||
name: "", //收货人姓名
|
||||
mobile: "", //手机号码
|
||||
consigneeAddressIdPath: [], //地址id
|
||||
consigneeAddressPath: [], //地址名字
|
||||
___path: "", //所在区域
|
||||
isDefault: false, //是否默认地址
|
||||
},
|
||||
// 表单提交校验规则
|
||||
rules: {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: "收货人姓名不能为空",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: "手机号码不能为空",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
return this.$u.test.mobile(value);
|
||||
},
|
||||
message: "手机号码不正确",
|
||||
trigger: ["change", "blur"],
|
||||
},
|
||||
],
|
||||
___path: [
|
||||
{
|
||||
required: true,
|
||||
message: "请选择所在区域",
|
||||
trigger: ["change"],
|
||||
},
|
||||
],
|
||||
detail: [
|
||||
{
|
||||
required: true,
|
||||
message: "请填写详细地址",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
],
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "",
|
||||
localName: "请选择",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
uni.showLoading({
|
||||
title: "加载中",
|
||||
});
|
||||
this.routerVal = option;
|
||||
// 如果当前是编辑地址,则需要查询出地址详情信息
|
||||
if (option.id) {
|
||||
getAddressDetail(option.id).then((res) => {
|
||||
const params = res.data.result;
|
||||
params.___path = params.consigneeAddressPath;
|
||||
this.$set(this, "form", params);
|
||||
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
}
|
||||
uni.hideLoading();
|
||||
},
|
||||
// 初始化rules必须要在onReady生命周期,因为onLoad生命周期组件可能尚未创建完毕
|
||||
onReady() {
|
||||
this.$refs.uForm.setRules(this.rules);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.detailAddress {
|
||||
/deep/ .u-form-item--left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
.saveBtn,
|
||||
.selectAddress {
|
||||
height: 70rpx;
|
||||
|
||||
line-height: 70rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
background: $aider-light-color;
|
||||
color: #fff;
|
||||
width: 70%;
|
||||
margin: 40rpx auto 0 auto;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
.selectAddress {
|
||||
margin-top: 40rpx;
|
||||
background: #fff;
|
||||
|
||||
color: $aider-light-color;
|
||||
border: 2rpx solid $aider-light-color;
|
||||
}
|
||||
|
||||
.uForm {
|
||||
width: 94%;
|
||||
overflow: hidden;
|
||||
left: 3%;
|
||||
position: relative;
|
||||
top: 2%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 0 0 40rpx 0;
|
||||
}
|
||||
.add-address {
|
||||
width: 100%;
|
||||
padding-top: 3%;
|
||||
|
||||
/deep/ .u-form-item {
|
||||
background-color: #fff;
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
.u-btn {
|
||||
margin: 30rpx 30rpx 0 30rpx;
|
||||
background-color: $main-color;
|
||||
}
|
||||
|
||||
/deep/.u-checkbox {
|
||||
margin: 30rpx 30rpx 0 30rpx;
|
||||
|
||||
.u-label-class.u-checkbox__label {
|
||||
color: $font-color-light;
|
||||
font-size: $font-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
/deep/ .u-checkbox__label {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view :style="themeStyle" class="address-page add-address">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="wechat-import">
|
||||
<button class="wechat-import-button" :disabled="importing" @click="importWechatAddress">
|
||||
<view class="wechat-icon"><u-icon name="weixin-fill" color="#07b75b" size="26" /></view>
|
||||
<view class="wechat-copy">
|
||||
<text class="wechat-title">{{ importing ? '正在导入微信地址…' : '导入微信收货地址' }}</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#77a38a" size="14" />
|
||||
</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view v-if="importNotice" class="import-notice">{{ importNotice }}</view>
|
||||
<view class="section-heading">收货信息</view>
|
||||
<view class="address-card form-card">
|
||||
<up-form
|
||||
:model="form"
|
||||
ref="uForm"
|
||||
error-type="toast"
|
||||
:rules="rules"
|
||||
label-position="left"
|
||||
label-width="180rpx"
|
||||
:label-style="{ color: '#5b606b', fontSize: '28rpx' }"
|
||||
>
|
||||
<up-form-item label="收货人" label-width="160rpx" prop="name" :border-bottom="true" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.name"
|
||||
border="none"
|
||||
input-align="left"
|
||||
clearable
|
||||
placeholder="姓名"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="手机号码" label-width="160rpx" prop="mobile" :border-bottom="true" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.mobile"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
input-align="left"
|
||||
placeholder="收货人的手机号码"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="所在地区" label-width="160rpx" prop="___path" :border-bottom="true" :custom-style="fieldStyle" @click="showPicker">
|
||||
<view class="form-value" :class="{ 'is-placeholder': !form.___path }">
|
||||
{{ form.___path || '请选择所在地区' }}
|
||||
</view>
|
||||
<template #right>
|
||||
<u-icon name="arrow-right" color="#ccc" size="16"></u-icon>
|
||||
</template>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="详细地址" label-width="160rpx" prop="detail" :border-bottom="true" :custom-style="{ ...fieldStyle, alignItems: 'flex-start' }">
|
||||
<textarea
|
||||
class="address-textarea"
|
||||
v-model="form.detail"
|
||||
maxlength="100"
|
||||
:auto-height="true"
|
||||
placeholder="街道、门牌号等详细信息"
|
||||
placeholder-style="color: #b1b5bd; font-size: 28rpx;"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
<up-form-item label="地址标签" label-width="160rpx" :border-bottom="false" :custom-style="fieldStyle">
|
||||
<u-input
|
||||
v-model="form.alias"
|
||||
border="none"
|
||||
input-align="left"
|
||||
placeholder="如:家、公司(选填)"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
</up-form>
|
||||
</view>
|
||||
<view class="address-card default-row">
|
||||
<view>
|
||||
<text class="default-title">设为默认地址</text>
|
||||
<text class="default-hint">下单时优先使用这个地址</text>
|
||||
</view>
|
||||
<switch :checked="!!form.isDefault" :color="mainColor" @change="form.isDefault = $event.detail.value" />
|
||||
</view>
|
||||
<view class="address-footer">
|
||||
<button class="address-primary-button" :disabled="importing" @click="save">保存收货地址</button>
|
||||
</view>
|
||||
|
||||
<m-city
|
||||
:provinceData="list"
|
||||
headTitle="区域选择"
|
||||
ref="cityPicker"
|
||||
@funcValue="getpickerParentValue"
|
||||
pickerSize="4"
|
||||
></m-city>
|
||||
|
||||
<uniMap v-if="mapFlag" @close="closeMap" @callback="callBackAddress" />
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, getCurrentInstance } from 'vue'
|
||||
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { addAddress as addAddressApi, editAddress, getAddressDetail } from '@/api/address.js'
|
||||
import MCity from '@/components/m-city/m-city.vue'
|
||||
import uniMap from '@/components/uniMap'
|
||||
import permision from '@/js_sdk/wa-permission/permission.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getRegionsById } from '@/api/common.js'
|
||||
import { resolveWechatRegion, getWechatAddressErrorType } from '@/utils/wechatAddress.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
const mainColor = computed(() => store.getters.mainColor)
|
||||
const fieldStyle = { padding: '28rpx 0', minHeight: '100rpx', boxSizing: 'border-box', alignItems: 'center' }
|
||||
const mapFlag = ref(false)
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const uForm = ref<any>(null)
|
||||
const cityPicker = ref<any>(null)
|
||||
const importing = ref(false)
|
||||
const importNotice = ref('')
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function importWechatAddress() {
|
||||
if (importing.value) return
|
||||
importing.value = true
|
||||
uni.chooseAddress({
|
||||
success: async (address: any) => {
|
||||
uni.showLoading({ title: '正在匹配地区', mask: true })
|
||||
// Clear the old region and coordinates, including when editing an existing address.
|
||||
Object.assign(form, {
|
||||
name: address.userName || '',
|
||||
mobile: address.telNumber || '',
|
||||
detail: address.detailInfoNew || address.detailInfo || '',
|
||||
consigneeAddressIdPath: [],
|
||||
consigneeAddressPath: [],
|
||||
___path: '',
|
||||
lat: '',
|
||||
lon: '',
|
||||
})
|
||||
importNotice.value = ''
|
||||
try {
|
||||
const regions = await resolveWechatRegion(address, getRegionsById)
|
||||
if (regions) {
|
||||
if (regions.length < 4 && address.streetName) {
|
||||
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||
}
|
||||
const names = regions.map((item: any) => item.name || item.localName)
|
||||
form.consigneeAddressIdPath = regions.map((item: any) => item.id)
|
||||
form.consigneeAddressPath = names
|
||||
form.___path = names.join('')
|
||||
const center = String(regions[regions.length - 1].center || '').split(',')
|
||||
if (center.length === 2) [form.lon, form.lat] = center
|
||||
importNotice.value = '已导入微信地址,请核对后保存'
|
||||
} else {
|
||||
showImportedRegionHint(address)
|
||||
}
|
||||
} catch {
|
||||
showImportedRegionHint(address)
|
||||
} finally {
|
||||
importing.value = false
|
||||
uni.hideLoading()
|
||||
uForm.value?.clearValidate()
|
||||
}
|
||||
},
|
||||
fail: (error: any) => {
|
||||
importing.value = false
|
||||
const type = getWechatAddressErrorType(error)
|
||||
if (type === 'cancel') return
|
||||
let platform = ''
|
||||
try { platform = (globalThis as any).wx?.getDeviceInfo?.()?.platform || '' } catch {}
|
||||
// Log diagnostics only, never the selected address or other personal information.
|
||||
console.warn('[微信地址导入失败]', {
|
||||
errMsg: String(error?.errMsg || error?.message || ''),
|
||||
errCode: error?.errCode ?? error?.errno ?? '',
|
||||
type,
|
||||
platform,
|
||||
})
|
||||
if (type === 'auth') {
|
||||
uni.showModal({
|
||||
title: '开启收货地址授权',
|
||||
content: '需要允许使用微信收货地址才能导入,也可以继续手动填写。',
|
||||
confirmText: '去设置',
|
||||
success: (result) => { if (result.confirm) uni.openSetting({}) },
|
||||
})
|
||||
return
|
||||
}
|
||||
if (type === 'privacy') {
|
||||
console.warn('[微信地址导入] 请核对小程序后台的用户隐私保护指引是否声明收货地址,以及用户隐私授权状态。')
|
||||
}
|
||||
if (type === 'configuration') {
|
||||
console.warn('[微信地址导入] manifest.json 的 mp-weixin.requiredPrivateInfos 必须包含 chooseAddress,修改后需重新编译小程序。')
|
||||
}
|
||||
const content = type === 'configuration'
|
||||
? '收货地址导入暂未配置完成,请稍后再试,也可以手动填写。'
|
||||
: type === 'privacy'
|
||||
? '暂时无法获取微信收货地址,请稍后重试,也可以手动填写。'
|
||||
: platform === 'devtools'
|
||||
? '当前开发工具中未能打开微信地址簿,请使用手机微信扫码预览测试,也可以手动填写。'
|
||||
: type === 'unsupported'
|
||||
? '当前微信环境不支持导入收货地址,请更新微信后重试,也可以手动填写。'
|
||||
: '暂时无法打开微信地址簿,请稍后重试,也可以手动填写。'
|
||||
uni.showModal({ title: '微信地址未导入', content, showCancel: false })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function showImportedRegionHint(address: any) {
|
||||
const region = [address.provinceName, address.cityName, address.countyName, address.streetName].filter(Boolean).join('')
|
||||
// Preserve the street if the user has to select it manually and the new detail field omits it.
|
||||
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||
importNotice.value = `已导入姓名、电话和详细地址,请补选所在地区${region ? '(微信地址:' + region + ')' : ''}`
|
||||
}
|
||||
// #endif
|
||||
|
||||
const form = reactive<Record<string, any>>({
|
||||
detail: '',
|
||||
name: '',
|
||||
mobile: '',
|
||||
consigneeAddressIdPath: [],
|
||||
consigneeAddressPath: [],
|
||||
___path: '',
|
||||
isDefault: false,
|
||||
})
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
id: '',
|
||||
localName: '请选择',
|
||||
children: [],
|
||||
},
|
||||
])
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '收货人姓名不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
message: '手机号码不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
{
|
||||
validator: (_rule: unknown, value: string) => proxy.$u.test.mobile(value),
|
||||
message: '手机号码不正确',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
___path: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择所在区域',
|
||||
trigger: ['change'],
|
||||
},
|
||||
],
|
||||
detail: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写详细地址',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
onShow(() => {})
|
||||
|
||||
onLoad(async (option) => {
|
||||
routerVal.value = option || {}
|
||||
uni.setNavigationBarTitle({ title: option?.id ? '编辑收货地址' : '新增收货地址' })
|
||||
if (!option?.id) return
|
||||
uni.showLoading({ title: '加载中' })
|
||||
try {
|
||||
const res = await getAddressDetail(option.id)
|
||||
const params = res.data?.result
|
||||
if (!res.data?.success || !params) {
|
||||
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Object.assign(form, params, { ___path: params.consigneeAddressPath || '' })
|
||||
} catch {
|
||||
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
uForm.value?.setRules(rules)
|
||||
})
|
||||
|
||||
function closeMap() {
|
||||
mapFlag.value = false
|
||||
}
|
||||
|
||||
function refuseMap() {
|
||||
uni.showModal({
|
||||
title: '温馨提示',
|
||||
content: '您已拒绝定位,请开启',
|
||||
confirmText: '去设置',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.getSystemInfo({
|
||||
success(sysRes) {
|
||||
if (sysRes.platform == 'ios') {
|
||||
plus.runtime.openURL('app-settings://')
|
||||
} else if (sysRes.platform == 'android') {
|
||||
const main = plus.android.runtimeMainActivity()
|
||||
const Intent = plus.android.importClass('android.content.Intent')
|
||||
const mIntent = new Intent('android.settings.ACTION_SETTINGS')
|
||||
main.startActivity(mIntent)
|
||||
}
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function requestAndroidPermission(permisionID: string) {
|
||||
const result = await permision.requestAndroidPermission(permisionID)
|
||||
if (result == 1) {
|
||||
mapFlag.value = true
|
||||
} else {
|
||||
refuseMap()
|
||||
}
|
||||
}
|
||||
|
||||
function callBackAddress(val: any) {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
if (val.regeocode && val) {
|
||||
const address = val.regeocode
|
||||
form.detail = address.formatted_address
|
||||
form.___path = val.data.result.name
|
||||
form.consigneeAddressIdPath = val.data.result.id
|
||||
form.consigneeAddressPath = val.data.result.name
|
||||
form.lat = val.latitude
|
||||
form.lon = val.longitude
|
||||
uni.hideLoading()
|
||||
}
|
||||
mapFlag.value = !mapFlag.value
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (importing.value) return
|
||||
uForm.value?.validate().then(() => {
|
||||
const params = { ...form }
|
||||
delete params.___path
|
||||
|
||||
if (Array.isArray(params.consigneeAddressIdPath)) {
|
||||
params.consigneeAddressIdPath = params.consigneeAddressIdPath.join(',')
|
||||
}
|
||||
if (Array.isArray(params.consigneeAddressPath)) {
|
||||
params.consigneeAddressPath = params.consigneeAddressPath.join(',')
|
||||
}
|
||||
|
||||
if (!params.id) {
|
||||
addAddressApi(params).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
delete params.updateBy
|
||||
delete params.updateTime
|
||||
editAddress(params).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function getpickerParentValue(e: any[]) {
|
||||
form.consigneeAddressIdPath = []
|
||||
form.consigneeAddressPath = []
|
||||
let name = ''
|
||||
e.forEach((item, index) => {
|
||||
if (item.id) {
|
||||
form.consigneeAddressIdPath.push(item.id)
|
||||
form.consigneeAddressPath.push(item.localName)
|
||||
name += item.localName
|
||||
form.___path = name
|
||||
}
|
||||
if (index == e.length - 1) {
|
||||
const center = String(item.center || item.children?.find((_child: any) => _child.id == item.id)?.center || '').split(',')
|
||||
form.lat = center.length === 2 ? center[1] : ''
|
||||
form.lon = center.length === 2 ? center[0] : ''
|
||||
}
|
||||
})
|
||||
importNotice.value = ''
|
||||
}
|
||||
|
||||
function showPicker() {
|
||||
cityPicker.value?.show()
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "./address-page.scss";
|
||||
.wechat-import { margin-bottom: 28rpx; }
|
||||
.wechat-import-button {
|
||||
display: flex; align-items: center; padding: 28rpx; margin: 0; width: 100%; text-align: left;
|
||||
background: #eef9f2; border-radius: 24rpx; line-height: 1.5;
|
||||
&::after { border: 1rpx solid #dcefe3; border-radius: 24rpx; }
|
||||
&[disabled] { background: #eef9f2; opacity: .65; }
|
||||
}
|
||||
.wechat-icon { width: 76rpx; height: 76rpx; border-radius: 20rpx; background: #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 22rpx; }
|
||||
.wechat-copy { flex: 1; min-width: 0; }
|
||||
.wechat-title { display: block; color: #253e30; font-size: 30rpx; font-weight: 600; }
|
||||
.import-notice { margin-bottom: 24rpx; padding: 20rpx 24rpx; border-radius: 16rpx; background: #fff5e5; color: #a47a3b; font-size: 24rpx; line-height: 1.6; }
|
||||
.section-heading { padding: 0 8rpx 20rpx; font-size: 26rpx; color: #858b96; }
|
||||
.form-card { padding: 0 28rpx; }
|
||||
.form-value { width: 100%; line-height: 1.6; font-size: 28rpx; color: #303744; overflow-wrap: anywhere; }
|
||||
.is-placeholder { color: #b1b5bd; }
|
||||
.address-textarea { width: 100%; min-height: 88rpx; font-size: 28rpx; color: #303744; line-height: 1.6; padding: 0; }
|
||||
.default-row { display: flex; justify-content: space-between; align-items: center; margin-top: 24rpx; padding: 28rpx; }
|
||||
.default-title { display: block; font-size: 28rpx; color: #353b47; }
|
||||
.default-hint { display: block; font-size: 23rpx; color: #a0a5af; margin-top: 8rpx; }
|
||||
.default-row switch { transform: scale(.8); transform-origin: right center; }
|
||||
</style>
|
||||
|
||||
43
pages/mine/address/address-page.scss
Normal file
43
pages/mine/address/address-page.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.address-page {
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
background: #f6f7f9;
|
||||
padding: 28rpx 24rpx calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.address-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.address-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 90;
|
||||
box-sizing: border-box;
|
||||
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #f0f1f4;
|
||||
}
|
||||
|
||||
.address-primary-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin: 0;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 44rpx;
|
||||
background: var(--theme-primary);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
&::after { border: none; }
|
||||
&[disabled] { opacity: .5; background: var(--theme-primary); color: #fff; }
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
padding: 2rpx 20rpx;
|
||||
}
|
||||
.list {
|
||||
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
|
||||
.item:hover {
|
||||
background: #ededed;
|
||||
}
|
||||
@@ -115,18 +118,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
padding: 20rpx 30rpx calc(20rpx + constant(safe-area-inset-bottom));
|
||||
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: $page-color-base;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: $light-color;
|
||||
position: fixed;
|
||||
width: 690rpx;
|
||||
bottom: 60rpx;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
left: 30rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 80rpx;
|
||||
|
||||
.u-icon {
|
||||
margin-right: 10rpx;
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.btn-text {
|
||||
margin-left: 12rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,135 @@
|
||||
<template>
|
||||
<view class="address">
|
||||
|
||||
<u-empty class="empty" v-if="addressList.length == 0" text="暂无收货地址" mode="address"></u-empty>
|
||||
<view class="list" v-else>
|
||||
<view class="item c-content" v-for="(item, index) in addressList" :key="index">
|
||||
<view class="basic" @click="selectAddressData(item)">
|
||||
<text>{{ item.name }}</text>
|
||||
<text>{{ item.mobile }}</text>
|
||||
<text class="default" v-show="item.isDefault">默认</text>
|
||||
<view>
|
||||
<div class="region">
|
||||
<span v-if="item.consigneeAddressPath[0]">{{item.consigneeAddressPath[0]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[1]">{{item.consigneeAddressPath[1]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[2]">{{item.consigneeAddressPath[2]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[3]">{{item.consigneeAddressPath[3]}}</span>
|
||||
<span>{{ item.detail }}</span>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
<view class="edit">
|
||||
<view class="relative" @click="setDefault(item)">
|
||||
<view v-if="item.isDefault" class="alifont icon-xuanzhong"></view>
|
||||
<text v-else class="unchecked"></text>
|
||||
<text>{{ item.isDefault ? "默认地址" : "设为默认" }}</text>
|
||||
</view>
|
||||
<view class="relative">
|
||||
<view class="alifont icon-bianji-copy"></view>
|
||||
<text class="mr-40" @click="addAddress(item.id)">编辑</text>
|
||||
<view class="alifont icon-lajitong"></view>
|
||||
<text @click="removeAddress(item.id)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: 100px"></view>
|
||||
</view>
|
||||
<button type="default" class="btn" @click="addAddress('')">
|
||||
<u-icon name="plus-circle"></u-icon>
|
||||
添加新收货人
|
||||
</button>
|
||||
<u-action-sheet :list="removeList" :tips="tips" v-model="showAction" @click="deleteAddressMessage"></u-action-sheet>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as API_Trade from "@/api/trade";
|
||||
import * as API_Address from "@/api/address.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
addressList: [], //地址列表
|
||||
showAction: false, //是否显示下栏框
|
||||
removeList: [
|
||||
{
|
||||
text: "确定",
|
||||
},
|
||||
],
|
||||
tips: {
|
||||
text: "确定要删除该收货人信息吗?",
|
||||
},
|
||||
removeId: "", //删除的地址id
|
||||
routerVal: "",
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
};
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
//下拉刷新
|
||||
this.addressList = [];
|
||||
this.getAddressList();
|
||||
},
|
||||
onLoad: function (val) {
|
||||
this.routerVal = val;
|
||||
},
|
||||
onShow() {
|
||||
this.addressList = [];
|
||||
this.getAddressList();
|
||||
},
|
||||
onHide() {},
|
||||
methods: {
|
||||
async selectAddressData(val) {
|
||||
await API_Trade.setAddressId(val.id, this.routerVal.way);
|
||||
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
},
|
||||
//获取地址列表
|
||||
getAddressList() {
|
||||
uni.showLoading();
|
||||
|
||||
API_Address.getAddressList(
|
||||
this.params.pageNumber,
|
||||
this.params.pageSize
|
||||
).then((res) => {
|
||||
res.data.result.records.forEach((item) => {
|
||||
item.consigneeAddressPath = item.consigneeAddressPath.split(",");
|
||||
});
|
||||
this.addressList = res.data.result.records;
|
||||
console.log(this.addressList);
|
||||
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
},
|
||||
//删除地址
|
||||
removeAddress(id) {
|
||||
this.removeId = id;
|
||||
this.showAction = true;
|
||||
},
|
||||
deleteAddressMessage() {
|
||||
API_Address.deleteAddress(this.removeId).then((res) => {
|
||||
if (res.statusCode == 200) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "删除成功",
|
||||
});
|
||||
this.getAddressList();
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: res.data.message,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
//新建。编辑地址
|
||||
addAddress(id) {
|
||||
if (id) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
"/pages/mine/address/add?id=" +
|
||||
id +
|
||||
"&way=" +
|
||||
this.routerVal.way +
|
||||
"&type=order",
|
||||
});
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
"/pages/mine/address/add?way=" + this.routerVal.way + "&type=order",
|
||||
});
|
||||
}
|
||||
},
|
||||
//设为默认地址
|
||||
setDefault(item) {
|
||||
delete item.updateBy;
|
||||
delete item.updateTime;
|
||||
delete item.deleteFlag;
|
||||
|
||||
item.isDefault ? "" : (item.isDefault = !item.isDefault);
|
||||
|
||||
API_Address.editAddress(item).then((res) => {
|
||||
uni.showToast({
|
||||
title: "设置默认地址成功",
|
||||
icon: "none",
|
||||
});
|
||||
this.getAddressList();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./address.scss";
|
||||
</style>
|
||||
<template>
|
||||
<view class="address">
|
||||
|
||||
<u-empty class="empty" v-if="addressList.length == 0" text="暂无收货地址" mode="address"></u-empty>
|
||||
<view class="list" v-else>
|
||||
<view class="item c-content" v-for="(item, index) in addressList" :key="index">
|
||||
<view class="basic" @click="selectAddressData(item)">
|
||||
<text>{{ item.name }}</text>
|
||||
<text>{{ item.mobile }}</text>
|
||||
<text class="default" v-show="item.isDefault">默认</text>
|
||||
<view>
|
||||
<div class="region">
|
||||
<span v-if="item.consigneeAddressPath[0]">{{item.consigneeAddressPath[0]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[1]">{{item.consigneeAddressPath[1]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[2]">{{item.consigneeAddressPath[2]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[3]">{{item.consigneeAddressPath[3]}}</span>
|
||||
<span>{{ item.detail }}</span>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
<view class="edit">
|
||||
<view class="relative" @click="setDefault(item)">
|
||||
<view v-if="item.isDefault" class="alifont icon-xuanzhong"></view>
|
||||
<text v-else class="unchecked"></text>
|
||||
<text>{{ item.isDefault ? "默认地址" : "设为默认" }}</text>
|
||||
</view>
|
||||
<view class="relative">
|
||||
<view class="alifont icon-bianji-copy"></view>
|
||||
<text class="mr-40" @click="addAddress(item.id)">编辑</text>
|
||||
<view class="alifont icon-lajitong" @click.stop="removeAddress(item.id)"></view>
|
||||
<text @click.stop="removeAddress(item.id)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: 100px"></view>
|
||||
</view>
|
||||
<view class="btn" @click="addAddress('')">
|
||||
<u-icon name="plus-circle" color="#ffffff" size="18"></u-icon>
|
||||
<text class="btn-text">添加新收货人</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import * as API_Trade from '@/api/trade'
|
||||
import * as API_Address from '@/api/address.js'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const addressList = ref<any[]>([])
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const params = { pageNumber: 1, pageSize: 1000 }
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
addressList.value = []
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
onLoad((val) => {
|
||||
routerVal.value = val || {}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
addressList.value = []
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
async function selectAddressData(val: any) {
|
||||
await API_Trade.setAddressId(val.id, routerVal.value.way)
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}
|
||||
|
||||
function getAddressList() {
|
||||
uni.showLoading()
|
||||
API_Address.getAddressList(params.pageNumber, params.pageSize).then((res) => {
|
||||
res.data.result.records.forEach((item: any) => {
|
||||
item.consigneeAddressPath = item.consigneeAddressPath.split(',')
|
||||
})
|
||||
addressList.value = res.data.result.records
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function removeAddress(id: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该收货人信息吗?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
API_Address.deleteAddress(id).then((result) => {
|
||||
if (result.statusCode == 200) {
|
||||
uni.showToast({ icon: 'none', title: '删除成功' })
|
||||
getAddressList()
|
||||
} else {
|
||||
uni.showToast({ icon: 'none', title: result.data.message, duration: 2000 })
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function addAddress(id: string) {
|
||||
if (id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/address/add?id=${id}&way=${routerVal.value.way}&type=order`,
|
||||
})
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/address/add?way=${routerVal.value.way}&type=order`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setDefault(item: any) {
|
||||
delete item.updateBy
|
||||
delete item.updateTime
|
||||
delete item.deleteFlag
|
||||
if (!item.isDefault) item.isDefault = true
|
||||
API_Address.editAddress(item).then(() => {
|
||||
uni.showToast({ title: '设置默认地址成功', icon: 'none' })
|
||||
getAddressList()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./address.scss";
|
||||
</style>
|
||||
|
||||
@@ -1,160 +1,171 @@
|
||||
<template>
|
||||
<view class="address">
|
||||
<u-empty class="empty" v-if="this.addressList.length === 0" text="暂无收货地址" mode="address"></u-empty>
|
||||
<view class="list" >
|
||||
<view class="item c-content" v-for="(item, index) in addressList" :key="index">
|
||||
<view class="basic">
|
||||
<text>{{ item.name }}</text>
|
||||
<text>{{ item.mobile }}</text>
|
||||
<text class="default" v-show="item.isDefault">默认</text>
|
||||
<view>
|
||||
<div class="region">
|
||||
<span v-if="item.consigneeAddressPath[0]">{{item.consigneeAddressPath[0]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[1]">{{item.consigneeAddressPath[1]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[2]">{{item.consigneeAddressPath[2]}}</span>
|
||||
<span v-if="item.consigneeAddressPath[3]">{{item.consigneeAddressPath[3]}}</span>
|
||||
<span>{{ item.detail }}</span>
|
||||
</div>
|
||||
</view>
|
||||
<view class="address-page address-manage" :style="themeStyle">
|
||||
<view v-if="!loading && addressList.length === 0" class="empty-state">
|
||||
<view class="empty-icon"><u-icon name="map" size="42" color="#c4c8d1" /></view>
|
||||
<text class="empty-title">还没有收货地址</text>
|
||||
<text class="empty-description">添加常用地址,下次购物更方便</text>
|
||||
</view>
|
||||
<view v-if="addressList.length" class="list-heading">
|
||||
<text>我的收货地址</text>
|
||||
<text class="address-count">共 {{ addressList.length }} 个</text>
|
||||
</view>
|
||||
<view class="address-list">
|
||||
<view class="address-card address-item" v-for="item in addressList" :key="item.id">
|
||||
<view class="recipient-line">
|
||||
<text class="recipient-name">{{ item.name }}</text>
|
||||
<text class="recipient-mobile">{{ item.mobile }}</text>
|
||||
</view>
|
||||
<view class="edit">
|
||||
<view class="relative" @click="setDefault(item)">
|
||||
<view v-if="item.isDefault" class="alifont icon-xuanzhong"></view>
|
||||
<text v-else class="unchecked"></text>
|
||||
<text>{{ item.isDefault ? "默认地址" : "设为默认" }}</text>
|
||||
<view class="address-region">{{ item.consigneeAddressPath.join(' ') }}</view>
|
||||
<text class="address-detail">{{ item.detail }}</text>
|
||||
<view class="address-tags" v-if="item.isDefault || item.alias">
|
||||
<text v-if="item.isDefault" class="address-tag default-tag">默认地址</text>
|
||||
<text v-if="item.alias" class="address-tag">{{ item.alias }}</text>
|
||||
</view>
|
||||
<view class="card-actions">
|
||||
<view class="default-action" @click="setDefault(item)">
|
||||
<view class="default-check" :class="{ 'is-checked': item.isDefault }">
|
||||
<u-icon v-if="item.isDefault" name="checkmark" size="11" color="#fff" />
|
||||
</view>
|
||||
<text>{{ item.isDefault ? '已设为默认' : '设为默认' }}</text>
|
||||
</view>
|
||||
<view class="relative">
|
||||
<view class="alifont icon-bianji-copy"></view>
|
||||
<text class="mr-40" @click="addAddress(item.id)">编辑</text>
|
||||
<view class="alifont icon-lajitong"></view>
|
||||
<text @click="removeAddress(item.id)">删除</text>
|
||||
<view class="edit-actions">
|
||||
<view class="text-action" @click="addAddress(item.id)">
|
||||
<u-icon name="edit-pen" size="16" color="#6c7280" /><text>编辑</text>
|
||||
</view>
|
||||
<view class="action-divider"></view>
|
||||
<view class="text-action" @click="removeAddress(item.id)">
|
||||
<u-icon name="trash" size="16" color="#9297a1" /><text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: 100px"></view>
|
||||
</view>
|
||||
|
||||
<button type="default" class="btn" @click="addAddress('')">
|
||||
<u-icon name="plus-circle"></u-icon>
|
||||
添加新收货人
|
||||
</button>
|
||||
|
||||
<u-action-sheet :list="removeList" :tips="tips" v-model="showAction" @click="deleteAddressMessage"></u-action-sheet>
|
||||
<view class="address-footer">
|
||||
<button class="address-primary-button" @click="addAddress('')">
|
||||
<u-icon name="plus" color="#fff" size="17" /><text>新增收货地址</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as API_Address from "@/api/address.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
addressList: [], //地址列表
|
||||
showAction: false, //是否显示下栏框
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { tipsToLogin } from '@/utils/filters.js'
|
||||
import * as API_Address from '@/api/address.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
removeList: [
|
||||
{
|
||||
text: "确定",
|
||||
},
|
||||
],
|
||||
tips: {
|
||||
text: "确定要删除该收货人信息吗?",
|
||||
},
|
||||
removeId: "", //删除的地址id
|
||||
routerVal: "",
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
};
|
||||
},
|
||||
// 返回上一级
|
||||
onBackPress(e) {
|
||||
uni.switchTab({
|
||||
url: "/pages/tabbar/user/my",
|
||||
});
|
||||
return true;
|
||||
},
|
||||
onLoad: function (val) {
|
||||
this.routerVal = val;
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
//下拉刷新
|
||||
this.addressList = [];
|
||||
this.getAddressList();
|
||||
},
|
||||
/**
|
||||
* 进入页面检测当前账户是否登录
|
||||
*/
|
||||
onShow() {
|
||||
if (this.$options.filters.tipsToLogin()) {
|
||||
this.getAddressList();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//获取地址列表
|
||||
getAddressList() {
|
||||
uni.showLoading();
|
||||
API_Address.getAddressList(
|
||||
this.params.pageNumber,
|
||||
this.params.pageSize
|
||||
).then((res) => {
|
||||
res.data.result.records.forEach((item) => {
|
||||
item.consigneeAddressPath = item.consigneeAddressPath.split(",");
|
||||
});
|
||||
this.addressList = res.data.result.records;
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
const loading = ref(true)
|
||||
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
},
|
||||
//删除地址
|
||||
removeAddress(id) {
|
||||
this.removeId = id;
|
||||
this.showAction = true;
|
||||
},
|
||||
// 删除地址
|
||||
deleteAddressMessage() {
|
||||
API_Address.deleteAddress(this.removeId).then((res) => {
|
||||
if (res.statusCode == 200) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "删除成功",
|
||||
});
|
||||
this.getAddressList();
|
||||
const addressList = ref<any[]>([])
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const params = { pageNumber: 1, pageSize: 1000 }
|
||||
|
||||
onBackPress(() => {
|
||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||
return true
|
||||
})
|
||||
|
||||
onLoad((val) => {
|
||||
routerVal.value = val || {}
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (tipsToLogin()) {
|
||||
getAddressList()
|
||||
}
|
||||
})
|
||||
|
||||
async function getAddressList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await API_Address.getAddressList(params.pageNumber, params.pageSize)
|
||||
if (!res.data?.success || !Array.isArray(res.data.result?.records)) throw new Error('地址加载失败')
|
||||
addressList.value = res.data.result.records.map((item: any) => ({
|
||||
...item,
|
||||
consigneeAddressPath: Array.isArray(item.consigneeAddressPath)
|
||||
? item.consigneeAddressPath
|
||||
: String(item.consigneeAddressPath || '').split(',').filter(Boolean),
|
||||
}))
|
||||
} catch {
|
||||
uni.showToast({ title: '地址加载失败,请下拉重试', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
function removeAddress(id: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该收货人信息吗?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
API_Address.deleteAddress(id).then((result) => {
|
||||
if (result.data?.success) {
|
||||
uni.showToast({ icon: 'none', title: '删除成功' })
|
||||
getAddressList()
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: res.data.message,
|
||||
duration: 2000,
|
||||
});
|
||||
uni.showToast({ icon: 'none', title: result.data.message, duration: 2000 })
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
//新建。编辑地址
|
||||
addAddress(id) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/address/add${id ? "?id=" + id : ""}`,
|
||||
});
|
||||
},
|
||||
//设为默认地址
|
||||
setDefault(item) {
|
||||
delete item.updateBy;
|
||||
delete item.updateTime;
|
||||
delete item.deleteFlag;
|
||||
})
|
||||
}
|
||||
|
||||
item.isDefault ? "" : (item.isDefault = !item.isDefault);
|
||||
function addAddress(id?: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/address/add${id ? '?id=' + id : ''}`,
|
||||
})
|
||||
}
|
||||
|
||||
API_Address.editAddress(item).then(() => {
|
||||
uni.showToast({
|
||||
title: "设置默认地址成功",
|
||||
icon: "none",
|
||||
});
|
||||
this.getAddressList();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
function setDefault(item: any) {
|
||||
if (item.isDefault) return
|
||||
const data = { ...item, isDefault: true, consigneeAddressPath: item.consigneeAddressPath.join(',') }
|
||||
delete data.updateBy
|
||||
delete data.updateTime
|
||||
delete data.deleteFlag
|
||||
API_Address.editAddress(data).then((res) => {
|
||||
if (res.data?.success) {
|
||||
uni.showToast({ title: '已设为默认地址', icon: 'none' })
|
||||
getAddressList()
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '设置失败,请重试', icon: 'none' })
|
||||
}
|
||||
}).catch(() => uni.showToast({ title: '设置失败,请重试', icon: 'none' }))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./address.scss";
|
||||
@import "./address-page.scss";
|
||||
|
||||
.list-heading { display: flex; justify-content: space-between; padding: 8rpx 8rpx 24rpx; font-size: 26rpx; color: #6c7280; }
|
||||
.address-count { color: #a0a5af; font-size: 24rpx; }
|
||||
.address-item { padding: 32rpx 32rpx 0; margin-bottom: 24rpx; }
|
||||
.recipient-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 20rpx; margin-bottom: 18rpx; }
|
||||
.recipient-name { font-size: 34rpx; font-weight: 600; color: #222732; overflow-wrap: anywhere; }
|
||||
.recipient-mobile { color: #636a77; font-size: 28rpx; }
|
||||
.address-region { color: #9399a4; font-size: 25rpx; line-height: 1.7; }
|
||||
.address-detail { display: block; color: #343b48; font-size: 29rpx; line-height: 1.7; overflow-wrap: anywhere; }
|
||||
.address-tags { display: flex; flex-wrap: wrap; gap: 12rpx; padding-top: 20rpx; }
|
||||
.address-tag { max-width: 100%; overflow-wrap: anywhere; padding: 4rpx 14rpx; background: #f3f4f6; border-radius: 8rpx; color: #7f8591; font-size: 22rpx; }
|
||||
.default-tag { color: var(--theme-primary); background: var(--theme-primary-10); }
|
||||
.card-actions { margin-top: 26rpx; border-top: 1rpx solid #f0f1f4; min-height: 92rpx; display: flex; justify-content: space-between; align-items: center; }
|
||||
.default-action, .text-action, .edit-actions { display: flex; align-items: center; }
|
||||
.default-action { gap: 12rpx; min-height: 80rpx; color: #858b96; font-size: 24rpx; }
|
||||
.default-check { width: 30rpx; height: 30rpx; box-sizing: border-box; border: 2rpx solid #dce0e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.is-checked { background: var(--theme-primary); border-color: var(--theme-primary); }
|
||||
.text-action { gap: 8rpx; min-height: 80rpx; color: #6c7280; font-size: 24rpx; padding: 0 8rpx; }
|
||||
.action-divider { width: 1rpx; height: 22rpx; background: #e9ebef; margin: 0 16rpx; }
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 160rpx 0 80rpx; }
|
||||
.empty-icon { width: 148rpx; height: 148rpx; display: flex; align-items: center; justify-content: center; background: #eaecef; border-radius: 50%; margin-bottom: 36rpx; }
|
||||
.empty-title { color: #454c59; font-size: 32rpx; font-weight: 500; }
|
||||
.empty-description { color: #a0a5af; font-size: 26rpx; margin-top: 16rpx; }
|
||||
</style>
|
||||
|
||||
@@ -16,71 +16,67 @@
|
||||
</view>
|
||||
<view style="height: 100px"></view>
|
||||
</view>
|
||||
<u-action-sheet :list="removeList" :tips="tips" v-model="showAction" @click="deleteAddressMessage"></u-action-sheet>
|
||||
<u-action-sheet
|
||||
:actions="removeList"
|
||||
:description="tips.text"
|
||||
cancel-text="取消"
|
||||
v-model:show="showAction"
|
||||
@select="deleteAddressMessage"
|
||||
></u-action-sheet>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as API_Trade from "@/api/trade";
|
||||
import * as API_Store from "@/api/store.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
storeAddressList: [], //地址列表
|
||||
showAction: false, //是否显示下栏框
|
||||
removeList: [
|
||||
{
|
||||
text: "确定",
|
||||
},
|
||||
],
|
||||
tips: {
|
||||
text: "确定要删除该收货人信息吗?",
|
||||
},
|
||||
removeId: "", //删除的地址id
|
||||
routerVal: "",
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 1000,
|
||||
},
|
||||
};
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
//下拉刷新
|
||||
this.storeAddressList = [];
|
||||
this.getAddressList();
|
||||
},
|
||||
onLoad: function (val) {
|
||||
this.routerVal = val;
|
||||
},
|
||||
onShow() {
|
||||
this.storeAddressList = [];
|
||||
this.getAddressList();
|
||||
},
|
||||
onHide() {},
|
||||
methods: {
|
||||
async selectAddressData(val) {
|
||||
await API_Trade.setStoreAddressId(val.id, this.routerVal.way);
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import * as API_Trade from '@/api/trade'
|
||||
import * as API_Store from '@/api/store.js'
|
||||
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
},
|
||||
//获取地址列表
|
||||
getAddressList() {
|
||||
uni.showLoading();
|
||||
const store = useStore()
|
||||
|
||||
API_Store.getStoreAddress(
|
||||
this.routerVal.storeId,
|
||||
this.params
|
||||
).then((res) => {
|
||||
this.storeAddressList = res.data.result.records;
|
||||
console.log(this.storeAddressList);
|
||||
const storeAddressList = ref<any[]>([])
|
||||
const showAction = ref(false)
|
||||
const removeList = [{ name: '确定' }]
|
||||
const tips = { text: '确定要删除该收货人信息吗?' }
|
||||
const removeId = ref('')
|
||||
const routerVal = ref<Record<string, string>>({})
|
||||
const params = { pageNumber: 1, pageSize: 1000 }
|
||||
|
||||
if (this.$store.state.isShowToast){ uni.hideLoading() };
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
onPullDownRefresh(() => {
|
||||
storeAddressList.value = []
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
onLoad((val) => {
|
||||
routerVal.value = val || {}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
storeAddressList.value = []
|
||||
getAddressList()
|
||||
})
|
||||
|
||||
function hideLoadingIfNeeded() {
|
||||
if (store.state.isShowToast) uni.hideLoading()
|
||||
}
|
||||
|
||||
async function selectAddressData(val: any) {
|
||||
await API_Trade.setStoreAddressId(val.id, routerVal.value.way)
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}
|
||||
|
||||
function getAddressList() {
|
||||
uni.showLoading()
|
||||
API_Store.getStoreAddress(routerVal.value.storeId, params).then((res) => {
|
||||
storeAddressList.value = res.data.result.records
|
||||
hideLoadingIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteAddressMessage() {
|
||||
// 保留 action-sheet 回调占位,当前页面无删除入口
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,219 +1,484 @@
|
||||
<template>
|
||||
<view class="wap">
|
||||
<u-navbar title="预存款列表">
|
||||
</u-navbar>
|
||||
<view class="wrapper-show-money">
|
||||
<view class="money-view">
|
||||
<h3>预存款金额 </h3>
|
||||
<view class="money">¥{{walletNum | unitPrice }}</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="wrapper-tabs">
|
||||
|
||||
<swiper class="swiper-box" :current="swiperCurrent">
|
||||
<swiper-item class="swiper-item" v-for="index in list.length" :key="index">
|
||||
<scroll-view class="scroll-v view-wrapper" enableBackToTop="true" scroll-with-animation scroll-y @scrolltolower="loadMore">
|
||||
<view v-if="depositData.length!=0" class="view-item" v-for="(logItem, logIndex) in depositData" :key="logIndex">
|
||||
<view class="view-item-detail">
|
||||
<view class="-title">{{logItem.detail}}</view>
|
||||
<!-- <view class="-number">{{logItem.detail}}</view> -->
|
||||
</view>
|
||||
<view class="view-item-change">
|
||||
<view class="-money green" v-if="logItem.serviceType == 'WALLET_PAY' || logItem.serviceType == 'WALLET_WITHDRAWAL'"> {{logItem.money | unitPrice}} </view>
|
||||
<view class="-money" v-if="logItem.serviceType == 'WALLET_REFUND' || logItem.serviceType == 'WALLET_RECHARGE' || logItem.serviceType == 'WALLET_COMMISSION' ">
|
||||
+{{logItem.money | unitPrice}} </view>
|
||||
<view class="-time">{{logItem.createTime}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<u-empty v-if="depositData.length==0" mode="history" text="暂无记录" />
|
||||
|
||||
</scroll-view>
|
||||
|
||||
</swiper-item>
|
||||
|
||||
</swiper>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserRecharge, getWalletLog } from "@/api/members";
|
||||
import { getUserWallet } from "@/api/members";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
walletNum: 0,
|
||||
current: 0,
|
||||
swiperCurrent: 0,
|
||||
userInfo: "", //用户详情信息
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
order: "desc",
|
||||
},
|
||||
depositData: [], //遍历的数据集合
|
||||
rechargeList: "", //充值明细列表
|
||||
walletLogList: "", //钱包变动列表
|
||||
list: [
|
||||
// {
|
||||
// name: "充值明细",
|
||||
// },
|
||||
{
|
||||
name: "预存款变动明细",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
swiperCurrent(index) {
|
||||
this.swiperCurrent = index;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.getWallet();
|
||||
let result = await getUserWallet(); //预存款
|
||||
|
||||
this.walletNum = result.data.result.memberWallet;
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**分页获取预存款充值记录 */
|
||||
getRecharge() {
|
||||
getUserRecharge(this.params).then((res) => {
|
||||
if (res.data.success) {
|
||||
if (res.data.result.records.length != 0) {
|
||||
this.depositData.push(...res.data.result.records);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getWallet() {
|
||||
|
||||
getWalletLog(this.params).then((res) => {
|
||||
if (res.data.success) {
|
||||
if (res.data.result.records.length != 0) {
|
||||
this.depositData.push(...res.data.result.records);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
changed(index) {
|
||||
this.depositData = [];
|
||||
this.swiperCurrent = index;
|
||||
this.params.pageNumber = 1;
|
||||
if (index == 0) {
|
||||
// this.getRecharge();
|
||||
this.getWallet();
|
||||
} else {
|
||||
this.getWallet();
|
||||
}
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
this.params.pageNumber++;
|
||||
this.getWallet();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.green {
|
||||
color: $aider-color-green !important;
|
||||
}
|
||||
.view-item {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.view-item-change {
|
||||
text-align: right;
|
||||
> .-money {
|
||||
font-size: 36rpx;
|
||||
color: $main-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
> .-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.view-item-detail {
|
||||
line-height: 1.75;
|
||||
> .-title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
> .-number {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.submit-btn {
|
||||
line-height: 90rpx;
|
||||
text-align: center;
|
||||
|
||||
color: #fff;
|
||||
background: $main-color;
|
||||
|
||||
margin: 0 auto;
|
||||
height: 90rpx;
|
||||
}
|
||||
|
||||
.operation {
|
||||
font-size: 32rpx;
|
||||
margin-right: 24rpx;
|
||||
color: rgb(96, 98, 102);
|
||||
}
|
||||
.money {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.money-view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 32rpx;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
background-image: linear-gradient(
|
||||
25deg,
|
||||
$main-color,
|
||||
$light-color,
|
||||
$aider-light-color
|
||||
);
|
||||
}
|
||||
|
||||
.swiper-item,
|
||||
.scroll-v {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
/* #ifndef H5 */
|
||||
height: calc(100vh - 200rpx);
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef H5 */
|
||||
height: calc(100vh - 288rpx);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.wap {
|
||||
width: 100%;
|
||||
height: calc(100vh - 44px);
|
||||
}
|
||||
|
||||
.wrapper-show-money {
|
||||
height: 200rpx;
|
||||
// background-image: url('/static/img/main-bg.jpg');
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view :style="themeStyle" class="deposit-log-page">
|
||||
<u-navbar
|
||||
title="预存款明细"
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:border="false"
|
||||
:auto-back="true"
|
||||
></u-navbar>
|
||||
|
||||
<!-- 顶部余额概览卡片 -->
|
||||
<view class="header-wrap">
|
||||
<view class="balance-hero">
|
||||
<view class="hero-glow hero-glow-1"></view>
|
||||
<view class="hero-glow hero-glow-2"></view>
|
||||
<view class="hero-top">
|
||||
<text class="money-label">当前预存款余额 (元)</text>
|
||||
</view>
|
||||
<view class="money-row">
|
||||
<text class="money-symbol">¥</text>
|
||||
<text class="money-value">{{ unitPrice(walletNum) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收支筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<view
|
||||
v-for="(tab, idx) in filterTabs"
|
||||
:key="tab.key"
|
||||
class="filter-pill"
|
||||
:class="{ active: activeFilter === idx }"
|
||||
@click="activeFilter = idx"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 明细列表区 -->
|
||||
<view class="list-container">
|
||||
<view v-if="filteredData.length > 0" class="list-card">
|
||||
<view
|
||||
class="log-item"
|
||||
v-for="(item, logIndex) in filteredData"
|
||||
:key="logIndex"
|
||||
>
|
||||
<!-- 第一行:类型标签 + 标题 | 变动金额 -->
|
||||
<view class="log-row-main">
|
||||
<view class="log-title-wrap">
|
||||
<text class="type-tag" :class="item.parsed.tagClass">{{ item.parsed.tagText }}</text>
|
||||
<text class="log-title">{{ item.parsed.title }}</text>
|
||||
</view>
|
||||
<text class="log-amount" :class="item.parsed.moneyClass">
|
||||
{{ item.parsed.formattedMoney }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:提取出的单号或备注说明 -->
|
||||
<view v-if="item.parsed.metaList.length > 0" class="log-meta-box">
|
||||
<view
|
||||
class="log-meta-line"
|
||||
v-for="(meta, mIdx) in item.parsed.metaList"
|
||||
:key="mIdx"
|
||||
>
|
||||
<text class="meta-label">{{ meta.label }}:</text>
|
||||
<text class="meta-val">{{ meta.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:变动时间 -->
|
||||
<view class="log-row-foot">
|
||||
<text class="log-time">{{ item.createTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loaded" class="empty-box">
|
||||
<u-empty mode="history" text="暂无预存款变动记录" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getWalletLog, getUserWallet } from '@/api/members'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const walletNum = ref(0)
|
||||
const loaded = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const activeFilter = ref(0)
|
||||
const filterTabs = [
|
||||
{ key: 'ALL', name: '全部明细' },
|
||||
{ key: 'IN', name: '收入 / 退款' },
|
||||
{ key: 'OUT', name: '支出 / 提现' },
|
||||
]
|
||||
|
||||
const params = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
order: 'desc',
|
||||
})
|
||||
const depositData = ref<any[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
getWallet()
|
||||
const result = await getUserWallet()
|
||||
walletNum.value = result.data?.result?.memberWallet ?? 0
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!hasMore.value) return
|
||||
loadMore()
|
||||
})
|
||||
|
||||
function isOutgoing(logItem: any) {
|
||||
const money = Number(logItem?.money) || 0
|
||||
if (money < 0) return true
|
||||
const serviceType = logItem?.serviceType
|
||||
return serviceType === 'WALLET_PAY' || serviceType === 'WALLET_WITHDRAWAL'
|
||||
}
|
||||
|
||||
function isIncoming(logItem: any) {
|
||||
const money = Number(logItem?.money) || 0
|
||||
if (money > 0) return true
|
||||
const serviceType = logItem?.serviceType
|
||||
return (
|
||||
serviceType === 'WALLET_REFUND' ||
|
||||
serviceType === 'WALLET_RECHARGE' ||
|
||||
serviceType === 'WALLET_COMMISSION'
|
||||
)
|
||||
}
|
||||
|
||||
function parseLogDetail(logItem: any) {
|
||||
const raw = String(logItem?.detail || '').trim()
|
||||
const serviceType = logItem?.serviceType || ''
|
||||
const out = isOutgoing(logItem)
|
||||
const inc = isIncoming(logItem)
|
||||
|
||||
const absAmount = unitPrice(Math.abs(Number(logItem?.money) || 0))
|
||||
const formattedMoney = out ? `-${absAmount}` : inc ? `+${absAmount}` : absAmount
|
||||
const moneyClass = out ? 'out' : inc ? 'in' : ''
|
||||
|
||||
let title = '余额变动'
|
||||
let tagText = '变动'
|
||||
let tagClass = 'tag-default'
|
||||
|
||||
const orderMatch = raw.match(/订单\[([^\]]+)\]/)
|
||||
const afterSaleMatch = raw.match(/售后单\[([^\]]+)\]/)
|
||||
const rechargeMatch = raw.match(/充值单号为[::]\s*([A-Za-z0-9_-]+)/)
|
||||
|
||||
if (afterSaleMatch && afterSaleMatch[1] === 'ORDER_CANCEL') {
|
||||
title = '订单取消退回'
|
||||
tagText = '退款'
|
||||
tagClass = 'tag-in'
|
||||
} else if (serviceType === 'WALLET_REFUND' || raw.includes('退还') || raw.includes('返还')) {
|
||||
title = '售后退款入账'
|
||||
tagText = '退款'
|
||||
tagClass = 'tag-in'
|
||||
} else if (serviceType === 'WALLET_PAY' || raw.includes('支付金额')) {
|
||||
title = '订单余额支付'
|
||||
tagText = '消费'
|
||||
tagClass = 'tag-out'
|
||||
} else if (serviceType === 'WALLET_RECHARGE') {
|
||||
title = raw.includes('后台') ? '平台余额充值' : '预存款充值'
|
||||
tagText = '充值'
|
||||
tagClass = 'tag-in'
|
||||
} else if (serviceType === 'WALLET_WITHDRAWAL') {
|
||||
title = out ? '余额提现申请' : '提现退回解冻'
|
||||
tagText = '提现'
|
||||
tagClass = out ? 'tag-out' : 'tag-in'
|
||||
} else if (serviceType === 'WALLET_COMMISSION') {
|
||||
title = '分销佣金入账'
|
||||
tagText = '佣金'
|
||||
tagClass = 'tag-in'
|
||||
}
|
||||
|
||||
const metaList: Array<{ label: string; value: string }> = []
|
||||
if (orderMatch && orderMatch[1]) {
|
||||
metaList.push({ label: '订单编号', value: orderMatch[1] })
|
||||
}
|
||||
if (afterSaleMatch && afterSaleMatch[1] && afterSaleMatch[1] !== 'ORDER_CANCEL') {
|
||||
metaList.push({ label: '售后单号', value: afterSaleMatch[1] })
|
||||
}
|
||||
if (rechargeMatch && rechargeMatch[1]) {
|
||||
metaList.push({ label: '充值单号', value: rechargeMatch[1] })
|
||||
}
|
||||
|
||||
if (metaList.length === 0 && raw && !raw.includes('???')) {
|
||||
const cleaned = raw
|
||||
.replace(/[,,]?\s*(支付金额|退还金额|余额返还)\[[^\]]+\]/g, '')
|
||||
.replace(/:\s*\d+(\.\d+)?$/, '')
|
||||
.trim()
|
||||
if (cleaned && cleaned !== title) {
|
||||
metaList.push({ label: '变动说明', value: cleaned })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
tagText,
|
||||
tagClass,
|
||||
formattedMoney,
|
||||
moneyClass,
|
||||
isOut: out,
|
||||
isIn: inc,
|
||||
metaList,
|
||||
}
|
||||
}
|
||||
|
||||
const decoratedList = computed(() =>
|
||||
depositData.value.map((item) => ({
|
||||
...item,
|
||||
parsed: parseLogDetail(item),
|
||||
}))
|
||||
)
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (activeFilter.value === 1) {
|
||||
return decoratedList.value.filter((item) => item.parsed.isIn)
|
||||
}
|
||||
if (activeFilter.value === 2) {
|
||||
return decoratedList.value.filter((item) => item.parsed.isOut)
|
||||
}
|
||||
return decoratedList.value
|
||||
})
|
||||
|
||||
function getWallet() {
|
||||
getWalletLog(params.value).then((res) => {
|
||||
loaded.value = true
|
||||
const records = (res.data?.success && res.data?.result?.records) || []
|
||||
if (records.length) {
|
||||
depositData.value.push(...records)
|
||||
}
|
||||
if (records.length < params.value.pageSize) {
|
||||
hasMore.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
params.value.pageNumber++
|
||||
getWallet()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.deposit-log-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding-bottom: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
padding: 24rpx 24rpx 8rpx;
|
||||
}
|
||||
|
||||
.balance-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
padding: 36rpx 36rpx 34rpx;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, $main-color 0%, $light-color 60%, $aider-light-color 100%);
|
||||
box-shadow: 0 12rpx 28rpx rgba(229, 50, 56, 0.18);
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-glow-1 {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
right: -60rpx;
|
||||
top: -70rpx;
|
||||
}
|
||||
|
||||
.hero-glow-2 {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
right: 90rpx;
|
||||
bottom: -60rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.hero-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.money-label {
|
||||
font-size: 25rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.money-row {
|
||||
margin-top: 14rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.money-symbol {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.money-value {
|
||||
font-size: 58rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 1rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 25rpx;
|
||||
color: #666666;
|
||||
background: #ffffff;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background: $main-color;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 16rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(17, 30, 79, 0.03);
|
||||
}
|
||||
|
||||
.log-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.log-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
line-height: 1.3;
|
||||
|
||||
&.tag-in {
|
||||
color: #00a870;
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
}
|
||||
|
||||
&.tag-out {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
|
||||
&.tag-default {
|
||||
color: #666666;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-amount {
|
||||
flex-shrink: 0;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #1d2129;
|
||||
|
||||
&.in {
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
&.out {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.log-meta-box {
|
||||
margin-top: 16rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
background: #f8f9fb;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.log-meta-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.45;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
flex-shrink: 0;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.meta-val {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #4e5969;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-row-foot {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
padding-top: 120rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
<template>
|
||||
<view class="wrapper">
|
||||
<u-cell-group>
|
||||
<u-cell-item title="申请单号"></u-cell-item>
|
||||
<u-cell-item title="提现金额" value="新版本"></u-cell-item>
|
||||
<u-cell-item title="收款银行"></u-cell-item>
|
||||
<u-cell-item title="收款账号" value="新版本"></u-cell-item>
|
||||
<u-cell-item title="开户人姓名"></u-cell-item>
|
||||
<u-cell-item title="创建时间" value="新版本"></u-cell-item>
|
||||
<u-cell-item title="提现状态" value="新版本"></u-cell-item>
|
||||
<u-cell title="申请单号"></u-cell>
|
||||
<u-cell title="提现金额" value="新版本"></u-cell>
|
||||
<u-cell title="收款银行"></u-cell>
|
||||
<u-cell title="收款账号" value="新版本"></u-cell>
|
||||
<u-cell title="开户人姓名"></u-cell>
|
||||
<u-cell title="创建时间" value="新版本"></u-cell>
|
||||
<u-cell title="提现状态" value="新版本"></u-cell>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,114 +1,168 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<u-navbar :custom-back="back" title="余额"></u-navbar>
|
||||
<div class="box">
|
||||
<div class="deposit">预存款金额</div>
|
||||
<div class="money">¥{{walletNum | unitPrice }}</div>
|
||||
<div class="operation-btns">
|
||||
<div class="operation-btn light" @click="navigateTo('/pages/mine/deposit/withdrawal')">提现</div>
|
||||
<div class="operation-btn" @click="navigateTo('/pages/mine/deposit/recharge')">充值</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" @click="navigateTo('/pages/mine/deposit/index')">
|
||||
<div class="list-left">预存款明细</div>
|
||||
<div class="list-right">
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<view :style="themeStyle" class="page">
|
||||
<u-navbar
|
||||
title="余额"
|
||||
:auto-back="false"
|
||||
:placeholder="true"
|
||||
:border="false"
|
||||
@leftClick="back"
|
||||
></u-navbar>
|
||||
|
||||
<view class="page-body">
|
||||
<view class="balance-card">
|
||||
<view class="deposit">预存款金额</view>
|
||||
<view class="money">¥{{ unitPrice(walletNum) }}</view>
|
||||
<view class="operation-btns">
|
||||
<view
|
||||
class="operation-btn light"
|
||||
@click="navigateTo('/pages/mine/deposit/withdrawal')"
|
||||
>提现</view>
|
||||
<view
|
||||
class="operation-btn primary"
|
||||
@click="navigateTo('/pages/mine/deposit/recharge')"
|
||||
>充值</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-list">
|
||||
<view class="list-item" @click="navigateTo('/pages/mine/deposit/index')">
|
||||
<text class="list-label">预存款明细</text>
|
||||
<u-icon name="arrow-right" color="#ccc" size="16"></u-icon>
|
||||
</view>
|
||||
<view class="list-item" @click="navigateTo('/pages/mine/deposit/withdrawApply')">
|
||||
<text class="list-label">提现记录</text>
|
||||
<u-icon name="arrow-right" color="#ccc" size="16"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserWallet } from "@/api/members";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
walletNum: 0,
|
||||
};
|
||||
},
|
||||
async onShow() {
|
||||
if (this.$options.filters.isLogin("auth")) {
|
||||
let result = await getUserWallet(); //预存款
|
||||
this.walletNum = result.data.result.memberWallet;
|
||||
} else {
|
||||
this.$options.filters.navigateToLogin("redirectTo");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
uni.switchTab({
|
||||
url: "/pages/tabbar/user/my",
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 跳转
|
||||
*/
|
||||
navigateTo(url) {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { getUserWallet } from '@/api/members'
|
||||
import { isLogin, navigateToLogin, unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import {
|
||||
prefetchMpMessageTemplates,
|
||||
subscribeMpMessageByScenes,
|
||||
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const walletNum = ref(0)
|
||||
|
||||
onShow(async () => {
|
||||
if (isLogin('auth')) {
|
||||
// #ifdef MP-WEIXIN
|
||||
prefetchMpMessageTemplates()
|
||||
// #endif
|
||||
const result = await getUserWallet()
|
||||
walletNum.value = result.data.result.memberWallet
|
||||
} else {
|
||||
navigateToLogin('redirectTo')
|
||||
}
|
||||
})
|
||||
|
||||
function back() {
|
||||
const doBack = () => {
|
||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
subscribeMpMessageByScenes(['WALLET_CHANGE'], doBack)
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
doBack()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.list-left {
|
||||
flex: 8;
|
||||
}
|
||||
.list-right {
|
||||
flex: 2;
|
||||
text-align: right;
|
||||
}
|
||||
.wrapper {
|
||||
width: 94%;
|
||||
margin: 0 3%;
|
||||
}
|
||||
.box {
|
||||
margin: 20rpx 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
|
||||
padding: 40rpx;
|
||||
.page-body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 48rpx 32rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.deposit {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.money {
|
||||
margin: 24rpx 0 40rpx;
|
||||
font-size: 64rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.operation-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.money {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 50rpx;
|
||||
margin: 20rpx 0 40rpx 0;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.deposit {
|
||||
margin-top: 50rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.operation-btn {
|
||||
background: #ee6d41;
|
||||
color: #fff;
|
||||
height: 90rpx;
|
||||
width: 240rpx;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 10rpx;
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.light {
|
||||
background: #fdf2ee;
|
||||
color: #ee6d41;
|
||||
color: $light-color;
|
||||
border: 1px solid rgba(238, 109, 65, 0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
.primary {
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
margin-top: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list-label {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,130 @@
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<view class="-list">
|
||||
<view :style="themeStyle" class="page">
|
||||
<view class="form-card">
|
||||
<view class="title">充值金额</view>
|
||||
<view class="content">
|
||||
<view class="price">
|
||||
<span> ¥</span>
|
||||
<u-input v-model="price" placeholder='金额' type="number" />
|
||||
|
||||
</view>
|
||||
|
||||
<view class="amount-row">
|
||||
<text class="currency">¥</text>
|
||||
<u-input
|
||||
v-model="price"
|
||||
type="digit"
|
||||
border="none"
|
||||
placeholder="请输入充值金额"
|
||||
input-align="left"
|
||||
class="amount-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit" :class="{'light':flag}" @click="handlerRecharge">充值</view>
|
||||
<view class="submit" :class="{ disabled: flag }" @click="handlerRecharge">充值</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { recharge } from "@/api/members";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
price: 0,
|
||||
flag: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
price(val) {
|
||||
val <= 0 ? (this.flag = true) : (this.flag = false);
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
// 充值
|
||||
async handlerRecharge() {
|
||||
if (this.price > 0) {
|
||||
|
||||
let res = await recharge({ price: this.price });
|
||||
if (res.data.success) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/cart/payment/payOrder?orderType=RECHARGE&recharge_sn=${res.data.result.rechargeSn}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { recharge } from '@/api/members'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const price = ref('')
|
||||
const flag = ref(true)
|
||||
|
||||
watch(price, (val) => {
|
||||
flag.value = !(Number(val) > 0)
|
||||
})
|
||||
|
||||
async function handlerRecharge() {
|
||||
const amount = Number(price.value)
|
||||
if (!(amount > 0)) {
|
||||
uni.showToast({ title: '请输入充值金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const res = await recharge({ price: amount })
|
||||
if (res.data.success) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/cart/payment/payOrder?orderType=RECHARGE&recharge_sn=${res.data.result.rechargeSn}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './style.scss';
|
||||
</style>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 0 8rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.currency {
|
||||
flex-shrink: 0;
|
||||
margin-right: 16rpx;
|
||||
font-size: 56rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content__field-wrapper__field) {
|
||||
height: 72rpx !important;
|
||||
min-height: 72rpx !important;
|
||||
font-size: 56rpx !important;
|
||||
font-weight: 600 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content__field-wrapper__field--placeholder) {
|
||||
font-size: 32rpx !important;
|
||||
font-weight: 400 !important;
|
||||
color: #c0c4cc !important;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 80rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/deep/ .u-input__input,
|
||||
::v-deep .u-input__input,
|
||||
.u-input {
|
||||
font-size: 80rpx !important;
|
||||
height: 102rpx !important;
|
||||
}
|
||||
/deep/ .u-input__input {
|
||||
::v-deep .u-input__input {
|
||||
height: 100%;
|
||||
font-size: 80rpx;
|
||||
}
|
||||
@@ -43,4 +43,4 @@
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
line-height: 90rpx;
|
||||
}
|
||||
}
|
||||
|
||||
487
pages/mine/deposit/withdrawApply.vue
Normal file
487
pages/mine/deposit/withdrawApply.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<view :style="themeStyle" class="withdraw-log-page">
|
||||
<u-navbar
|
||||
title="提现记录"
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:border="false"
|
||||
:auto-back="false"
|
||||
@leftClick="back"
|
||||
></u-navbar>
|
||||
|
||||
<!-- 状态筛选胶囊栏 -->
|
||||
<view class="filter-bar">
|
||||
<view
|
||||
v-for="(tab, idx) in filterTabs"
|
||||
:key="tab.key"
|
||||
class="filter-pill"
|
||||
:class="{ active: activeFilter === idx }"
|
||||
@click="activeFilter = idx"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现记录列表 -->
|
||||
<view class="list-container">
|
||||
<view v-if="filteredRecords.length > 0" class="list-card">
|
||||
<view
|
||||
class="record-card"
|
||||
v-for="(item, index) in filteredRecords"
|
||||
:key="item.id || index"
|
||||
>
|
||||
<!-- 第一行:状态标签 + 标题 | 提现金额 -->
|
||||
<view class="card-row-main">
|
||||
<view class="title-wrap">
|
||||
<text class="status-tag" :class="item.parsed.statusClass">
|
||||
{{ item.parsed.statusText }}
|
||||
</text>
|
||||
<text class="card-title">余额提现</text>
|
||||
</view>
|
||||
<view class="amount-wrap">
|
||||
<text class="amount-symbol">¥</text>
|
||||
<text class="amount-val">{{ unitPrice(item.applyMoney) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:单号、审核备注或失败原因 -->
|
||||
<view
|
||||
v-if="item.sn || item.inspectRemark || item.errorMessage || item.inspectTime"
|
||||
class="meta-box"
|
||||
>
|
||||
<view v-if="item.sn" class="meta-line">
|
||||
<text class="meta-label">提现单号:</text>
|
||||
<text class="meta-val mono">{{ item.sn }}</text>
|
||||
</view>
|
||||
<view v-if="item.inspectTime" class="meta-line">
|
||||
<text class="meta-label">审核时间:</text>
|
||||
<text class="meta-val">{{ item.inspectTime }}</text>
|
||||
</view>
|
||||
<view v-if="item.inspectRemark || item.errorMessage" class="meta-line">
|
||||
<text class="meta-label">备注说明:</text>
|
||||
<text class="meta-val remark-text">{{ item.inspectRemark || item.errorMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:申请时间 + 确认收款按钮 -->
|
||||
<view class="card-row-foot">
|
||||
<text class="apply-time">申请时间:{{ item.createTime || item.inspectTime || '-' }}</text>
|
||||
<view
|
||||
v-if="item.applyStatus === 'WAIT_USER_CONFIRM'"
|
||||
class="confirm-btn"
|
||||
@click.stop="confirmWechatReceive(item)"
|
||||
>
|
||||
确认收款
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loaded" class="empty-box">
|
||||
<u-empty mode="history" text="暂无提现记录" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getWithdrawApplyPage, getWithdrawApplyWechatTransferInfo } from '@/api/members'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import {
|
||||
prefetchMpMessageTemplates,
|
||||
subscribeMpMessageByScenes,
|
||||
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const loaded = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const activeFilter = ref(0)
|
||||
const filterTabs = [
|
||||
{ key: 'ALL', name: '全部' },
|
||||
{ key: 'PENDING', name: '处理中' },
|
||||
{ key: 'SUCCESS', name: '已到账' },
|
||||
{ key: 'FAIL', name: '未通过/失败' },
|
||||
]
|
||||
|
||||
const params = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
order: 'desc',
|
||||
})
|
||||
const records = ref<any[]>([])
|
||||
|
||||
onShow(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 预拉取订阅模板,保证点击左上角返回时可同步拉起微信订阅弹窗
|
||||
prefetchMpMessageTemplates()
|
||||
// #endif
|
||||
reloadData()
|
||||
})
|
||||
|
||||
// 左上角返回:提示订阅「提现结果通知」后再返回
|
||||
function back() {
|
||||
const doBack = () => {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/mine/deposit/operation' })
|
||||
}
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
subscribeMpMessageByScenes(['WALLET_WITHDRAWAL_AUDIT_ERROR'], doBack)
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
doBack()
|
||||
// #endif
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!hasMore.value) return
|
||||
loadMore()
|
||||
})
|
||||
|
||||
function reloadData() {
|
||||
params.value.pageNumber = 1
|
||||
hasMore.value = true
|
||||
records.value = []
|
||||
loaded.value = false
|
||||
getData()
|
||||
}
|
||||
|
||||
function parseWithdrawStatus(applyStatus: string) {
|
||||
switch (applyStatus) {
|
||||
case 'APPLY':
|
||||
return { statusText: '申请中', statusClass: 'tag-pending', group: 'PENDING' }
|
||||
case 'VIA_AUDITING':
|
||||
return { statusText: '审核通过', statusClass: 'tag-pending', group: 'PENDING' }
|
||||
case 'D_VIA_AUDITING':
|
||||
return { statusText: '分销审核通过', statusClass: 'tag-pending', group: 'PENDING' }
|
||||
case 'WAIT_USER_CONFIRM':
|
||||
return { statusText: '待确认收款', statusClass: 'tag-action', group: 'PENDING' }
|
||||
case 'SUCCESS':
|
||||
return { statusText: '提现成功', statusClass: 'tag-success', group: 'SUCCESS' }
|
||||
case 'FAIL_AUDITING':
|
||||
return { statusText: '审核未通过', statusClass: 'tag-fail', group: 'FAIL' }
|
||||
case 'D_FAIL_AUDITING':
|
||||
return { statusText: '分销审核未通过', statusClass: 'tag-fail', group: 'FAIL' }
|
||||
case 'ERROR':
|
||||
return { statusText: '提现失败', statusClass: 'tag-fail', group: 'FAIL' }
|
||||
default:
|
||||
return { statusText: applyStatus || '处理中', statusClass: 'tag-pending', group: 'PENDING' }
|
||||
}
|
||||
}
|
||||
|
||||
const decoratedRecords = computed(() =>
|
||||
records.value.map((item) => ({
|
||||
...item,
|
||||
parsed: parseWithdrawStatus(item.applyStatus),
|
||||
}))
|
||||
)
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
const targetKey = filterTabs[activeFilter.value]?.key || 'ALL'
|
||||
if (targetKey === 'ALL') return decoratedRecords.value
|
||||
return decoratedRecords.value.filter((item) => item.parsed.group === targetKey)
|
||||
})
|
||||
|
||||
function getData() {
|
||||
getWithdrawApplyPage(params.value).then((res) => {
|
||||
loaded.value = true
|
||||
const list = (res.data?.success && res.data?.result?.records) || []
|
||||
if (list.length > 0) {
|
||||
records.value.push(...list)
|
||||
}
|
||||
if (list.length < params.value.pageSize) {
|
||||
hasMore.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
params.value.pageNumber++
|
||||
getData()
|
||||
}
|
||||
|
||||
function confirmWechatReceive(item: any) {
|
||||
const id = item && item.id
|
||||
if (!id) {
|
||||
uni.showToast({ title: '缺少提现记录ID', duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '加载中' })
|
||||
getWithdrawApplyWechatTransferInfo(id)
|
||||
.then((res) => {
|
||||
if (!res.data || !res.data.success) return
|
||||
|
||||
const info = res.data.result || {}
|
||||
const mchId = info.mchId
|
||||
const wechatPackage = info.wechatPackage
|
||||
let appId = info.appId
|
||||
|
||||
if (typeof wx !== 'undefined' && wx.getAccountInfoSync) {
|
||||
try {
|
||||
const accountInfo = wx.getAccountInfoSync()
|
||||
const mpAppId = accountInfo && accountInfo.miniProgram && accountInfo.miniProgram.appId
|
||||
if (mpAppId) appId = mpAppId
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!mchId || !appId || !wechatPackage) {
|
||||
uni.showToast({ title: '微信确认参数缺失', duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const openResultToast = (errMsg?: string) => {
|
||||
if (errMsg === 'requestMerchantTransfer:ok') {
|
||||
uni.showToast({ title: '已确认收款', duration: 2000, icon: 'none' })
|
||||
setTimeout(() => reloadData(), 800)
|
||||
return
|
||||
}
|
||||
if (errMsg === 'requestMerchantTransfer:cancel') {
|
||||
uni.showToast({ title: '已取消', duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: errMsg ? `唤起失败:${errMsg}` : '唤起失败',
|
||||
duration: 2500,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
if (sys && sys.platform === 'devtools') {
|
||||
uni.showToast({
|
||||
title: '开发者工具可能不支持,请真机测试',
|
||||
duration: 2500,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (typeof wx !== 'undefined' && wx.canIUse && wx.canIUse('requestMerchantTransfer')) {
|
||||
wx.requestMerchantTransfer({
|
||||
mchId,
|
||||
appId,
|
||||
package: wechatPackage,
|
||||
success: (r: any) => openResultToast(r && (r.errMsg || r.err_msg)),
|
||||
fail: (r: any) => openResultToast(r && (r.errMsg || r.err_msg)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof WeixinJSBridge !== 'undefined' && WeixinJSBridge.invoke) {
|
||||
WeixinJSBridge.invoke(
|
||||
'requestMerchantTransfer',
|
||||
{ mchId, appId, package: wechatPackage },
|
||||
(r: any) => openResultToast(r && (r.errMsg || r.err_msg))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '请在微信内打开确认收款', duration: 2000, icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
uni.hideLoading()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.withdraw-log-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding-bottom: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 24rpx 8rpx;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 25rpx;
|
||||
color: #666666;
|
||||
background: #ffffff;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background: $main-color;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
padding: 16rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(17, 30, 79, 0.03);
|
||||
}
|
||||
|
||||
.card-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
line-height: 1.35;
|
||||
|
||||
&.tag-pending {
|
||||
color: #ff7d00;
|
||||
background: rgba(255, 125, 0, 0.1);
|
||||
}
|
||||
|
||||
&.tag-action {
|
||||
color: $main-color;
|
||||
background: rgba(229, 50, 56, 0.1);
|
||||
}
|
||||
|
||||
&.tag-success {
|
||||
color: #00a870;
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
}
|
||||
|
||||
&.tag-fail {
|
||||
color: #f53f3f;
|
||||
background: rgba(245, 63, 63, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.amount-wrap {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.amount-symbol {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.amount-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.meta-box {
|
||||
margin-top: 16rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
background: #f8f9fb;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.45;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
flex-shrink: 0;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.meta-val {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #4e5969;
|
||||
word-break: break-all;
|
||||
|
||||
&.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
&.remark-text {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row-foot {
|
||||
margin-top: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.apply-time {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 10rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, $main-color 0%, $light-color 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.22);
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
padding-top: 140rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +1,349 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="-list">
|
||||
<view class="title">提现类型</view>
|
||||
<view class="content">
|
||||
<view class="price">
|
||||
<u-input disabled :value="type === 'ALI' ? '支付宝' : '微信'" placeholder="" />
|
||||
</view>
|
||||
</view>
|
||||
<view :style="themeStyle" class="page">
|
||||
<view class="form-card">
|
||||
<view class="field-label">提现类型</view>
|
||||
<view class="readonly-value">{{ typeLabel }}</view>
|
||||
</view>
|
||||
<view class="-list">
|
||||
<view class="title">提现金额</view>
|
||||
<view class="content">
|
||||
<view class="price">
|
||||
<span> ¥</span>
|
||||
<u-input v-model="price" placeholder="" type="number" />
|
||||
</view>
|
||||
|
||||
<view class="all">
|
||||
<view @click="handleAll" :style="{ color: $mainColor }">全部</view>
|
||||
<view style="font-size: 24rpx; color: #999">可提现金额<span>{{ walletNum | unitPrice }}</span>元</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="tips">
|
||||
最低提现金额为 {{ minPrice }} 元
|
||||
</view>
|
||||
</view>
|
||||
<view class="-list" v-if="type === 'ALI'">
|
||||
<view class="title">真实姓名</view>
|
||||
<view class="content">
|
||||
<view class="price">
|
||||
<u-input v-model="realName" placeholder="" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="-list" v-if="type === 'ALI'">
|
||||
<view class="title">第三方登录账号</view>
|
||||
<view class="content">
|
||||
<view class="price">
|
||||
<u-input v-model="connectNumber" placeholder="" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="field-label">提现金额</view>
|
||||
<view class="amount-main">
|
||||
<view class="amount-left">
|
||||
<text class="currency">¥</text>
|
||||
<u-input
|
||||
v-model="price"
|
||||
type="digit"
|
||||
border="none"
|
||||
placeholder="请输入提现金额"
|
||||
input-align="left"
|
||||
class="amount-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="amount-extra">
|
||||
<text class="all-btn" @click="handleAll">全部</text>
|
||||
<text class="balance-tip">可提现金额 {{ unitPrice(walletNum) }} 元</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tips">最低提现金额为 {{ minPrice }} 元</view>
|
||||
</view>
|
||||
|
||||
<template v-if="type === 'ALI'">
|
||||
<view class="form-card">
|
||||
<view class="field-row">
|
||||
<text class="field-label">真实姓名</text>
|
||||
<u-input
|
||||
v-model="realName"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="请输入真实姓名"
|
||||
class="field-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-card">
|
||||
<view class="field-row">
|
||||
<text class="field-label">第三方登录账号</text>
|
||||
<u-input
|
||||
v-model="connectNumber"
|
||||
border="none"
|
||||
input-align="right"
|
||||
placeholder="请输入支付宝账号"
|
||||
class="field-input"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="submit" @click="cashd">提现</view>
|
||||
|
||||
<!-- 提现规则说明(满足微信小程序审核要求) -->
|
||||
<view class="rule-card">
|
||||
<view class="rule-title">
|
||||
<text class="rule-icon"></text>
|
||||
<text>提现规则说明</text>
|
||||
</view>
|
||||
<view class="rule-content">
|
||||
<text class="rule-text">{{ displayRule }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { getUserWallet, withdrawalApply, withdrawalSettingVO } from "@/api/members";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
price: 0,
|
||||
walletNum: 0,
|
||||
minPrice: 0,
|
||||
type: '',
|
||||
connectNumber: '',
|
||||
realName: ''
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let result = await getUserWallet(); //预存款
|
||||
let res = await withdrawalSettingVO();
|
||||
this.walletNum = result.data.result.memberWallet;
|
||||
this.minPrice = res.data.result.minPrice;
|
||||
this.type = res.data.result.type;
|
||||
},
|
||||
|
||||
methods: {
|
||||
cashd() {
|
||||
this.price = this.price + "";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance, onMounted } from 'vue'
|
||||
import { getUserWallet, withdrawalApply, withdrawalSettingVO } from '@/api/members'
|
||||
import { unitPrice } from '@/utils/filters.js'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
if (this.$u.test.amount(parseInt(this.price))) {
|
||||
let params = { price: this.price };
|
||||
if (this.type === 'ALI') {
|
||||
if (!this.connectNumber || !this.realName) {
|
||||
uni.showToast({
|
||||
title: "请输入真实姓名和第三方登录账号",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
params.connectNumber = this.connectNumber;
|
||||
params.realName = this.realName;
|
||||
}
|
||||
withdrawalApply(params).then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({
|
||||
title: "提现成功!",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "请输入正确金额",
|
||||
duration: 2000,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
handleAll() {
|
||||
this.price = this.walletNum;
|
||||
},
|
||||
},
|
||||
};
|
||||
const { proxy } = getCurrentInstance()!
|
||||
|
||||
const price = ref('')
|
||||
const walletNum = ref(0)
|
||||
const minPrice = ref(0)
|
||||
const type = ref('')
|
||||
const connectNumber = ref('')
|
||||
const realName = ref('')
|
||||
const withdrawRule = ref('')
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
if (type.value === 'ALI') return '支付宝'
|
||||
if (type.value) return '微信'
|
||||
return '--'
|
||||
})
|
||||
|
||||
const defaultRule = computed(() => {
|
||||
const minText = minPrice.value ? `单笔最低提现金额为 ${minPrice.value} 元` : '单笔最低提现金额以页面提示为准'
|
||||
const channelText = typeLabel.value === '支付宝' ? '支付宝账户' : '微信零钱账户'
|
||||
return `1. 提现额度:${minText},单日累计最高提现 5000 元;
|
||||
2. 提现频次:每个用户每日最多可申请提现 1 次;
|
||||
3. 提现时间:支持全天 24 小时随时发起提现申请;
|
||||
4. 到账时间:提交申请后预计 1~3 个工作日内审核并转账至您的${channelText};
|
||||
5. 手续费说明:提现免收手续费;
|
||||
6. 如有其他疑问或遇到提现未到账,请联系在线客服协助处理。`
|
||||
})
|
||||
|
||||
const displayRule = computed(() => {
|
||||
return withdrawRule.value ? withdrawRule.value : defaultRule.value
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const result = await getUserWallet()
|
||||
const res = await withdrawalSettingVO()
|
||||
walletNum.value = result.data.result.memberWallet
|
||||
minPrice.value = res.data.result.minPrice
|
||||
type.value = res.data.result.type
|
||||
withdrawRule.value = res.data.result.withdrawRule || ''
|
||||
})
|
||||
|
||||
let submitting = false
|
||||
|
||||
function cashd() {
|
||||
if (submitting) return
|
||||
const amount = Number(price.value)
|
||||
if (!(amount > 0) || !proxy.$u.test.amount(parseInt(String(amount)))) {
|
||||
uni.showToast({ title: '请输入正确金额', duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (minPrice.value && amount < Number(minPrice.value)) {
|
||||
uni.showToast({ title: `最低提现金额为 ${minPrice.value} 元`, duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (amount > Number(walletNum.value || 0)) {
|
||||
uni.showToast({ title: '提现金额不能大于可提现金额', duration: 2000, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const params: Record<string, any> = { price: amount }
|
||||
if (type.value === 'ALI') {
|
||||
if (!connectNumber.value || !realName.value) {
|
||||
uni.showToast({
|
||||
title: '请输入真实姓名和第三方登录账号',
|
||||
duration: 2000,
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
params.connectNumber = connectNumber.value
|
||||
params.realName = realName.value
|
||||
}
|
||||
|
||||
const doSubmit = () => {
|
||||
submitting = true
|
||||
withdrawalApply(params)
|
||||
.then((res) => {
|
||||
if (res.data.success) {
|
||||
uni.showToast({ title: '提现申请已提交', duration: 2000, icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitting = false
|
||||
})
|
||||
}
|
||||
|
||||
doSubmit()
|
||||
}
|
||||
|
||||
function handleAll() {
|
||||
price.value = String(walletNum.value || '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./style.scss";
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
margin-top: 20rpx;
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.amount-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-top: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.amount-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.currency {
|
||||
flex-shrink: 0;
|
||||
margin-right: 12rpx;
|
||||
font-size: 56rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.amount-extra {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-left: 24rpx;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.all-btn {
|
||||
font-size: 28rpx;
|
||||
color: $main-color;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.balance-tip {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input),
|
||||
:deep(.field-input .u-input) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content),
|
||||
:deep(.field-input .u-input__content) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content__field-wrapper__field) {
|
||||
height: 72rpx !important;
|
||||
min-height: 72rpx !important;
|
||||
font-size: 56rpx !important;
|
||||
font-weight: 600 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
:deep(.amount-input .u-input__content__field-wrapper__field--placeholder) {
|
||||
font-size: 32rpx !important;
|
||||
font-weight: 400 !important;
|
||||
color: #c0c4cc !important;
|
||||
}
|
||||
|
||||
:deep(.field-input .u-input__content__field-wrapper__field) {
|
||||
font-size: 28rpx !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 56rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: $light-color;
|
||||
color: #fff;
|
||||
border-radius: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
margin-top: 48rpx;
|
||||
padding: 12rpx;
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.rule-icon {
|
||||
width: 6rpx;
|
||||
height: 26rpx;
|
||||
background: $main-color;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-content {
|
||||
.rule-text {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,568 @@
|
||||
<template>
|
||||
|
||||
<view class="page" :style="themeStyle">
|
||||
<view v-if="!pageReady" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll" :show-scrollbar="false">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
v-for="item in rangeTabs"
|
||||
:key="item.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeRange === item.value }"
|
||||
@click="changeRange(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="custom-time" :class="{ active: activeRange === 'CUSTOM' }" @click="openCustomPicker">
|
||||
<text>自定义时间</text>
|
||||
<u-icon name="arrow-down" size="12" :color="activeRange === 'CUSTOM' ? '#ff8f3f' : '#999'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="state-wrap">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="content">
|
||||
<view class="summary-card">
|
||||
<view class="summary-label-row">
|
||||
<text class="summary-label">累计收益</text>
|
||||
<view class="info-icon" @click="showGuide = true">
|
||||
<u-icon name="info-circle" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="summary-amount">
|
||||
<text class="amount-value">{{ formatMoney(stats.totalEarnings) }}</text>
|
||||
<text class="amount-unit">元</text>
|
||||
</view>
|
||||
<view class="summary-pending">含待结算 {{ formatMoney(stats.pendingEarnings) }}元</view>
|
||||
</view>
|
||||
|
||||
<view class="earn-grid">
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">已结算收益</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.settledEarnings) }}<text class="unit">元</text></view>
|
||||
</view>
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">商品佣金</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.directCommission) }}<text class="unit">元</text></view>
|
||||
<view class="earn-sub">含待结算: {{ formatMoney(stats.directPending) }}元</view>
|
||||
</view>
|
||||
<view class="earn-item">
|
||||
<view class="earn-title">邀请奖励</view>
|
||||
<view class="earn-value">{{ formatMoney(stats.inviteReward) }}<text class="unit">元</text></view>
|
||||
<view class="earn-sub">含待结算: {{ formatMoney(stats.invitePending) }}元</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计销售额(元)</view>
|
||||
<view class="stats-value">{{ formatMoney(stats.totalSales) }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计订单(笔)</view>
|
||||
<view class="stats-value">{{ stats.totalOrders || 0 }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计客户(人)</view>
|
||||
<view class="stats-value">{{ stats.totalCustomers || 0 }}</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-label">累计邀请(人)</view>
|
||||
<view class="stats-value">{{ stats.totalInvites || 0 }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-link" @click="showGuide = true">查看业绩指标说明</view>
|
||||
</view>
|
||||
|
||||
<u-popup v-model:show="customVisible" mode="bottom" round="16">
|
||||
<view class="custom-popup">
|
||||
<view class="popup-title">自定义时间</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">开始日期</text>
|
||||
<picker mode="date" :value="customStartDate" @change="onStartDateChange">
|
||||
<view class="picker-value">{{ customStartDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-row">
|
||||
<text class="popup-label">结束日期</text>
|
||||
<picker mode="date" :value="customEndDate" @change="onEndDateChange">
|
||||
<view class="picker-value">{{ customEndDate || '请选择' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<view class="popup-btn ghost" @click="customVisible = false">取消</view>
|
||||
<view class="popup-btn primary" @click="applyCustomRange">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<u-popup v-model:show="showGuide" mode="center" round="16" :safe-area-inset-bottom="false">
|
||||
<view class="guide-popup">
|
||||
<view class="guide-title">业绩指标说明</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">销售额</view>
|
||||
<view class="guide-desc">仅统计推广订单的销售金额总和</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">客户</view>
|
||||
<view class="guide-desc">与分销员建立绑定关系的买家</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">邀请</view>
|
||||
<view class="guide-desc">分销员成功邀请的下级分销员</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">商品佣金</view>
|
||||
<view class="guide-desc">分销员推荐客户购买商品后获得的佣金</view>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<view class="guide-term">邀请奖励</view>
|
||||
<view class="guide-desc">下级分销员推广商品后,上级获得的奖励</view>
|
||||
</view>
|
||||
<view class="guide-btn" @click="showGuide = false">知道了</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow, onLoad } from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import { getDistributionPerformance } from '@/api/distribution'
|
||||
import { assertActiveDistributor } from '@/utils/distributionStatus'
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const rangeTabs = [
|
||||
{ label: '全部', value: 'ALL' },
|
||||
{ label: '今日', value: 'TODAY' },
|
||||
{ label: '昨日', value: 'YESTERDAY' },
|
||||
{ label: '近七日', value: 'LAST_7_DAYS' },
|
||||
]
|
||||
|
||||
const pageReady = ref(false)
|
||||
const activeRange = ref('ALL')
|
||||
const loading = ref(false)
|
||||
const customVisible = ref(false)
|
||||
const showGuide = ref(false)
|
||||
const customStartDate = ref('')
|
||||
const customEndDate = ref('')
|
||||
|
||||
const stats = ref({
|
||||
totalEarnings: 0,
|
||||
pendingEarnings: 0,
|
||||
settledEarnings: 0,
|
||||
directCommission: 0,
|
||||
directPending: 0,
|
||||
inviteReward: 0,
|
||||
invitePending: 0,
|
||||
totalSales: 0,
|
||||
totalOrders: 0,
|
||||
totalCustomers: 0,
|
||||
totalInvites: 0,
|
||||
})
|
||||
|
||||
const RANGE_VALUES = ['ALL', 'TODAY', 'YESTERDAY', 'LAST_7_DAYS', 'CUSTOM']
|
||||
|
||||
onLoad((options: Record<string, string>) => {
|
||||
if (options?.rangeType && RANGE_VALUES.includes(options.rangeType)) {
|
||||
activeRange.value = options.rangeType
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
assertActiveDistributor().then((d) => {
|
||||
if (!d) {
|
||||
return
|
||||
}
|
||||
pageReady.value = true
|
||||
loadPerformance()
|
||||
})
|
||||
})
|
||||
|
||||
function formatMoney(val: number | string) {
|
||||
return Number(val || 0).toFixed(2)
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const params: Record<string, string> = { rangeType: activeRange.value }
|
||||
if (activeRange.value === 'CUSTOM') {
|
||||
if (customStartDate.value) {
|
||||
params.startTime = `${customStartDate.value} 00:00:00`
|
||||
}
|
||||
if (customEndDate.value) {
|
||||
params.endTime = `${customEndDate.value} 23:59:59`
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function loadPerformance() {
|
||||
loading.value = true
|
||||
getDistributionPerformance(buildParams())
|
||||
.then((res) => {
|
||||
const data = res.data?.result || {}
|
||||
stats.value = {
|
||||
totalEarnings: Number(data.totalEarnings || 0),
|
||||
pendingEarnings: Number(data.pendingEarnings || 0),
|
||||
settledEarnings: Number(data.settledEarnings || 0),
|
||||
directCommission: Number(data.directCommission || 0),
|
||||
directPending: Number(data.directPending || 0),
|
||||
inviteReward: Number(data.inviteReward || 0),
|
||||
invitePending: Number(data.invitePending || 0),
|
||||
totalSales: Number(data.totalSales || 0),
|
||||
totalOrders: Number(data.totalOrders || 0),
|
||||
totalCustomers: Number(data.totalCustomers || 0),
|
||||
totalInvites: Number(data.totalInvites || 0),
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeRange(value: string) {
|
||||
if (activeRange.value === value) return
|
||||
activeRange.value = value
|
||||
loadPerformance()
|
||||
}
|
||||
|
||||
function openCustomPicker() {
|
||||
customVisible.value = true
|
||||
}
|
||||
|
||||
function onStartDateChange(e: { detail: { value: string } }) {
|
||||
customStartDate.value = e.detail.value
|
||||
}
|
||||
|
||||
function onEndDateChange(e: { detail: { value: string } }) {
|
||||
customEndDate.value = e.detail.value
|
||||
}
|
||||
|
||||
function applyCustomRange() {
|
||||
if (!customStartDate.value || !customEndDate.value) {
|
||||
uni.showToast({ title: '请选择开始和结束日期', icon: 'none' })
|
||||
return
|
||||
}
|
||||
activeRange.value = 'CUSTOM'
|
||||
customVisible.value = false
|
||||
loadPerformance()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 88rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #ff8f3f;
|
||||
background: #fff3ea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-left: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.custom-time.active {
|
||||
color: #ff8f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.summary-card,
|
||||
.earn-grid,
|
||||
.stats-grid {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 32rpx 28rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.summary-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.amount-unit {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.summary-pending {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.earn-grid {
|
||||
display: flex;
|
||||
padding: 28rpx 0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.earn-item {
|
||||
flex: 1;
|
||||
padding: 0 20rpx;
|
||||
text-align: center;
|
||||
border-right: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.earn-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.earn-title {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.earn-value {
|
||||
margin-top: 12rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.earn-value .unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.earn-sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
padding: 32rpx 28rpx;
|
||||
border-right: 1rpx solid #f0f0f0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-item:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.stats-item:nth-last-child(-n + 2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
margin-top: 16rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.guide-link {
|
||||
margin-top: 40rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.custom-popup {
|
||||
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-btn.ghost {
|
||||
background: #f5f6f8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.popup-btn.primary {
|
||||
background: linear-gradient(135deg, #ff9f43, #ff7f27);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-popup {
|
||||
width: 620rpx;
|
||||
padding: 40rpx 36rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #222;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.guide-term {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.guide-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.guide-btn {
|
||||
margin-top: 12rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 42rpx;
|
||||
background: linear-gradient(135deg, #ff9f43, #ff7f27);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user