更新若干功能
This commit is contained in:
6
App.vue
6
App.vue
@@ -61,6 +61,8 @@ export default {
|
||||
).then((result) => {
|
||||
setCache("token", result.token);
|
||||
setCache("user_info", result.user_info);
|
||||
// 详情页可能抢在登录前请求过,通知它们带 p_user_id 再绑一次
|
||||
uni.$emit("wx-login-ready");
|
||||
});
|
||||
} else {
|
||||
console.error("登录失败!" + loginRes.errMsg);
|
||||
@@ -77,4 +79,8 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import "uview-plus/index.scss";
|
||||
/* PC 限宽后两侧露出来的底,跟默认主题背景对齐 */
|
||||
page {
|
||||
background-color: #FAF7F2;
|
||||
}
|
||||
</style>
|
||||
31
api/page/address.js
Normal file
31
api/page/address.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { get, post } from '../request'
|
||||
|
||||
const prefix = 'address/'
|
||||
|
||||
export const getAddressListApi = async () => {
|
||||
return await get(`${prefix}list`)
|
||||
}
|
||||
|
||||
export const getDefaultAddressApi = async () => {
|
||||
return await get(`${prefix}default-one`)
|
||||
}
|
||||
|
||||
export const getAddressDetailApi = async (id) => {
|
||||
return await get(`${prefix}detail`, { id })
|
||||
}
|
||||
|
||||
export const createAddressApi = async (params) => {
|
||||
return await post(`${prefix}create`, params)
|
||||
}
|
||||
|
||||
export const updateAddressApi = async (params) => {
|
||||
return await post(`${prefix}update`, params)
|
||||
}
|
||||
|
||||
export const deleteAddressApi = async (id) => {
|
||||
return await post(`${prefix}delete`, { ids: [id] })
|
||||
}
|
||||
|
||||
export const setDefaultAddressApi = async (id) => {
|
||||
return await post(`${prefix}set-default`, { id })
|
||||
}
|
||||
11
api/page/afterSale.js
Normal file
11
api/page/afterSale.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { get, post } from '../request'
|
||||
|
||||
const prefix = 'after-sale/'
|
||||
|
||||
export const getAfterSaleListApi = async () => {
|
||||
return await get(`${prefix}list`)
|
||||
}
|
||||
|
||||
export const createAfterSaleApi = async (params) => {
|
||||
return await post(`${prefix}create`, params)
|
||||
}
|
||||
@@ -48,3 +48,8 @@ export const updateItemApi = async (id, params = {}) => {
|
||||
...params
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出清单报价文本/CSV */
|
||||
export const exportQuoteApi = async (id) => {
|
||||
return await get(`${prefix}export-quote`, { id })
|
||||
}
|
||||
|
||||
19
api/page/colorcard.js
Normal file
19
api/page/colorcard.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { get } from '../request'
|
||||
|
||||
const prefix = 'colorcard/'
|
||||
|
||||
export const getColorcardListApi = async (params = {}) => {
|
||||
return await get(`${prefix}list`, params)
|
||||
}
|
||||
|
||||
export const getColorcardDetailApi = async (id) => {
|
||||
return await get(`${prefix}detail`, { id })
|
||||
}
|
||||
|
||||
export const getColorcardClassListApi = async () => {
|
||||
return await get(`${prefix}class-list`)
|
||||
}
|
||||
|
||||
export const getColorcardCompanyListApi = async () => {
|
||||
return await get(`${prefix}company-list`)
|
||||
}
|
||||
15
api/page/factory.js
Normal file
15
api/page/factory.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { get } from '../request'
|
||||
|
||||
const prefix = 'factory/'
|
||||
|
||||
export const getFactoryListApi = async (params = {}) => {
|
||||
return await get(`${prefix}list`, params)
|
||||
}
|
||||
|
||||
export const getFactoryDetailApi = async (id) => {
|
||||
return await get(`${prefix}detail`, { id })
|
||||
}
|
||||
|
||||
export const getFactoryClassListApi = async () => {
|
||||
return await get(`${prefix}class-list`)
|
||||
}
|
||||
11
api/page/favorite.js
Normal file
11
api/page/favorite.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { get, post } from '../request'
|
||||
|
||||
const prefix = 'favorite/'
|
||||
|
||||
export const getFavoriteListApi = async () => {
|
||||
return await get(`${prefix}list`)
|
||||
}
|
||||
|
||||
export const toggleFavoriteApi = async (catalogueId) => {
|
||||
return await post(`${prefix}toggle`, { catalogue_id: catalogueId })
|
||||
}
|
||||
11
api/page/feedback.js
Normal file
11
api/page/feedback.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { get, post } from '../request'
|
||||
|
||||
const prefix = 'feedback/'
|
||||
|
||||
export const getFeedbackListApi = async () => {
|
||||
return await get(`${prefix}list`)
|
||||
}
|
||||
|
||||
export const createFeedbackApi = async (params) => {
|
||||
return await post(`${prefix}create`, params)
|
||||
}
|
||||
@@ -15,3 +15,7 @@ export const updateNickNameApi = async (nick_name) => {
|
||||
nick_name
|
||||
})
|
||||
}
|
||||
|
||||
export const getEnterpriseApi = async () => {
|
||||
return await get(`${prefix}enterprise`)
|
||||
}
|
||||
|
||||
@@ -97,12 +97,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 画布坐标按 340×420 设计稿写成百分比,列变窄时图层一起缩小,不会撑破格子。
|
||||
*/
|
||||
layerStyle(layer) {
|
||||
return {
|
||||
left: (layer.x || 0) + 'rpx',
|
||||
top: (layer.y || 0) + 'rpx',
|
||||
width: (layer.w || 40) + 'rpx',
|
||||
height: (layer.h || 40) + 'rpx',
|
||||
left: (((layer.x || 0) / 340) * 100) + '%',
|
||||
top: (((layer.y || 0) / 420) * 100) + '%',
|
||||
width: (((layer.w || 40) / 340) * 100) + '%',
|
||||
height: (((layer.h || 40) / 420) * 100) + '%',
|
||||
transform: 'rotate(' + (layer.rotate || 0) + 'deg)',
|
||||
zIndex: layer.z || 1,
|
||||
}
|
||||
@@ -169,17 +172,22 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tcc-fallback {
|
||||
width: 100%;
|
||||
}
|
||||
.tcc {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
height: 0;
|
||||
padding-bottom: 123.53%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tcc-stage {
|
||||
position: relative;
|
||||
width: 340rpx;
|
||||
height: 420rpx;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tcc-layer {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<view class="tn">
|
||||
<!-- PC 大屏不支持自定义顶栏,留给微信拖拽区;标题写到系统导航栏 -->
|
||||
<view v-if="!pcMode" class="tn">
|
||||
<view class="tn-fixed" :class="'tn--' + mode" :style="barStyle">
|
||||
<view class="tn-status" :style="{ height: statusBarHeight + 'px' }" />
|
||||
<view class="tn-inner" :style="{ height: innerHeight + 'px' }">
|
||||
<view class="tn-status" :style="statusStyle" />
|
||||
<view class="tn-inner" :style="innerStyle">
|
||||
<view class="tn-left" @tap="onBack">
|
||||
<u-icon v-if="showBack" name="arrow-left" :color="text" size="20" />
|
||||
</view>
|
||||
@@ -12,12 +13,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-ph" :style="{ height: navHeight + 'px' }" />
|
||||
<view class="tn-ph" :style="phStyle" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getActiveLayout, getActiveVars } from '@/utils/theme'
|
||||
import { isPcWeixin } from '@/utils/device'
|
||||
|
||||
/**
|
||||
* 自定义顶栏:色跟主题 token,形态跟 chrome.navbar
|
||||
@@ -32,6 +34,8 @@ export default {
|
||||
return {
|
||||
statusBarHeight: 20,
|
||||
innerHeight: 44,
|
||||
// 量不到胶囊时按常见胶囊宽度让开,避免右侧按钮被挡住
|
||||
capsulePadRight: 96,
|
||||
mode: 'solid',
|
||||
surface: '#ffffff',
|
||||
text: '#1C1917',
|
||||
@@ -39,6 +43,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pcMode() {
|
||||
return isPcWeixin()
|
||||
},
|
||||
navHeight() {
|
||||
return this.statusBarHeight + this.innerHeight
|
||||
},
|
||||
@@ -48,10 +55,41 @@ export default {
|
||||
borderColor: this.border,
|
||||
}
|
||||
},
|
||||
statusStyle() {
|
||||
return {
|
||||
height: this.statusBarHeight + 'px',
|
||||
}
|
||||
},
|
||||
/** 占位跟固定栏同高,用像素避免 env(safe-area) 在安卓微信为 0 时切内容 */
|
||||
phStyle() {
|
||||
return {
|
||||
height: this.navHeight + 'px',
|
||||
width: '100%',
|
||||
}
|
||||
},
|
||||
innerStyle() {
|
||||
return {
|
||||
height: this.innerHeight + 'px',
|
||||
paddingRight: this.capsulePadRight + 'px',
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
title: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.syncPcTitle(val)
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.measure()
|
||||
this.pullTheme()
|
||||
this.syncPcTitle(this.title)
|
||||
},
|
||||
mounted() {
|
||||
// 部分机型 created 时 statusBarHeight 还是 0,挂载后再量一次
|
||||
this.measure()
|
||||
},
|
||||
beforeUpdate() {
|
||||
this.pullTheme()
|
||||
@@ -64,6 +102,8 @@ export default {
|
||||
const menu = uni.getMenuButtonBoundingClientRect && uni.getMenuButtonBoundingClientRect()
|
||||
if (menu && menu.height) {
|
||||
this.innerHeight = (menu.top - this.statusBarHeight) * 2 + menu.height
|
||||
const winW = Number(sys.windowWidth) || 375
|
||||
this.capsulePadRight = Math.max(8, Math.round(winW - menu.left + 8))
|
||||
}
|
||||
} catch (err) {
|
||||
this.statusBarHeight = 20
|
||||
@@ -77,6 +117,21 @@ export default {
|
||||
this.surface = vars['--color-surface'] || '#ffffff'
|
||||
this.text = vars['--color-text'] || '#1C1917'
|
||||
this.border = vars['--color-border'] || '#E7DFD3'
|
||||
this.syncPcBarColor()
|
||||
},
|
||||
/** 系统导航栏底色跟主题表面色对齐,避免 PC 顶栏发灰 */
|
||||
syncPcBarColor() {
|
||||
if (!isPcWeixin()) return
|
||||
const bg = this.surface || '#ffffff'
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: '#000000',
|
||||
backgroundColor: bg,
|
||||
})
|
||||
},
|
||||
/** PC 走系统导航栏,动态标题(如编辑地址)要写过去 */
|
||||
syncPcTitle(val) {
|
||||
if (!isPcWeixin() || !val) return
|
||||
uni.setNavigationBarTitle({ title: String(val) })
|
||||
},
|
||||
onBack() {
|
||||
if (!this.showBack) return
|
||||
@@ -92,6 +147,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.tn-ph {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tn-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -105,20 +169,35 @@ export default {
|
||||
.tn-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16rpx;
|
||||
position: relative;
|
||||
padding-left: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tn-left,
|
||||
.tn-right {
|
||||
.tn-left {
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.tn-right {
|
||||
width: auto;
|
||||
min-width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.tn-title {
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
left: 160rpx;
|
||||
right: 160rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"urlCheck" : false,
|
||||
"minified" : true
|
||||
},
|
||||
// "resizable" : true,
|
||||
"resizable" : true,
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 商品瀑布流通用 mixin
|
||||
*
|
||||
* product.vue / search.vue 的瀑布流高度同构:data 字段、左右两列 computed、
|
||||
* product.vue / search.vue 的瀑布流高度同构:data 字段、按列数拆列 computed、
|
||||
* 触底加载更多、下拉刷新这些样板代码两边一模一样。这里抽公共,避免改一处忘改另一处。
|
||||
*
|
||||
* 约定(引入本 mixin 的页面必须遵守):
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
page: 1,
|
||||
// 每页条数,与后端约定 10
|
||||
pageSize: 10,
|
||||
// 商品列表(扁平数组),computed 会拆成左右两列
|
||||
// 商品列表(扁平数组),computed 按 pcGoodsCols 拆成 N 列
|
||||
products: [],
|
||||
// 首屏 loading(控制骨架屏显隐)
|
||||
loading: false,
|
||||
@@ -33,13 +33,17 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 偶数下标 → 左列
|
||||
leftProducts() {
|
||||
return this.products.filter((_, index) => index % 2 === 0)
|
||||
},
|
||||
// 奇数下标 → 右列
|
||||
rightProducts() {
|
||||
return this.products.filter((_, index) => index % 2 === 1)
|
||||
/**
|
||||
* 按当前窗口列数把商品轮流分进各列,宽屏一行更多而不是两列撑满。
|
||||
* 依赖 theme mixin 的 pcGoodsCols;两个列表页都混了 theme。
|
||||
*/
|
||||
productColumns() {
|
||||
const n = Math.max(2, Number(this.pcGoodsCols) || 2)
|
||||
const cols = Array.from({ length: n }, () => [])
|
||||
this.products.forEach((item, index) => {
|
||||
cols[index % n].push(item)
|
||||
})
|
||||
return cols
|
||||
},
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
|
||||
@@ -7,6 +7,14 @@ import {
|
||||
loadAndApplyTheme,
|
||||
} from '@/utils/theme'
|
||||
import { resolvePageTabBar, syncTabBarByRoute } from '@/utils/tabbar'
|
||||
import {
|
||||
PC_PAGE_MAX_WIDTH,
|
||||
catColsByWidth,
|
||||
getWindowWidth,
|
||||
goodsColsByWidth,
|
||||
isPcWeixin,
|
||||
watchWindowWidth,
|
||||
} from '@/utils/device'
|
||||
|
||||
/**
|
||||
* 页面主题 mixin:暴露色板 + layout + 进入动效 class
|
||||
@@ -20,18 +28,39 @@ export default {
|
||||
themeMeta: getThemeMeta(),
|
||||
themeSchemes: getActiveSchemes(),
|
||||
themeFeatures: getThemeFeatures(),
|
||||
pcWindowWidth: getWindowWidth(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPcWeixin() {
|
||||
return isPcWeixin()
|
||||
},
|
||||
/** 首页 / 列表商品列数,随窗口宽度变(不限 PC) */
|
||||
pcGoodsCols() {
|
||||
return goodsColsByWidth(this.pcWindowWidth)
|
||||
},
|
||||
/** 首页分类宫格列数,格子更小所以更早加列 */
|
||||
pcCatCols() {
|
||||
return catColsByWidth(this.pcWindowWidth)
|
||||
},
|
||||
themeEffect() {
|
||||
return (this.themeLayout && this.themeLayout.effect) || {}
|
||||
},
|
||||
themePageStyle() {
|
||||
const v = this.themeVars || {}
|
||||
return {
|
||||
const style = {
|
||||
backgroundColor: v['--color-bg'] || '#F7F8FA',
|
||||
color: v['--color-text'] || '#1a1a1a',
|
||||
}
|
||||
// PC 大窗不要把手机版拉成一条超宽带,内容限宽居中
|
||||
if (isPcWeixin()) {
|
||||
style.maxWidth = PC_PAGE_MAX_WIDTH + 'px'
|
||||
style.marginLeft = 'auto'
|
||||
style.marginRight = 'auto'
|
||||
style.width = '100%'
|
||||
style.boxSizing = 'border-box'
|
||||
}
|
||||
return style
|
||||
},
|
||||
themePrimary() {
|
||||
return (this.themeVars && this.themeVars['--color-primary']) || '#B08D57'
|
||||
@@ -108,7 +137,17 @@ export default {
|
||||
)
|
||||
},
|
||||
packageList() {
|
||||
return this.sectionVariant('package', 'list', 'card')
|
||||
return this.sectionVariant(
|
||||
'package',
|
||||
'list',
|
||||
(this.themeLayout.package && this.themeLayout.package.list) || 'card',
|
||||
)
|
||||
},
|
||||
/** 套餐页搜索:优先本页 search 区块,没有则复用首页搜索样式 */
|
||||
packageSearch() {
|
||||
const own = this.sectionVariant('package', 'search', '')
|
||||
if (own) return own
|
||||
return this.homeSearch
|
||||
},
|
||||
packageEnabled() {
|
||||
return !!(this.themeFeatures && this.themeFeatures.package_enabled)
|
||||
@@ -193,16 +232,21 @@ export default {
|
||||
homeSearch() {
|
||||
return this.sectionVariant('home', 'search', 'bar')
|
||||
},
|
||||
/** 当前页皮肤:先读本页 pages.xxx.skin,没有再回落到首页 */
|
||||
pageSkin() {
|
||||
return this.pageMeta('home').skin || 'shop'
|
||||
const page = this.pageMeta(this.currentThemePage || 'home')
|
||||
const home = this.pageMeta('home')
|
||||
return page.skin || home.skin || 'shop'
|
||||
},
|
||||
pageDensity() {
|
||||
const page = this.currentThemePage || 'home'
|
||||
return this.pageMeta(page).density || 'regular'
|
||||
const page = this.pageMeta(this.currentThemePage || 'home')
|
||||
const home = this.pageMeta('home')
|
||||
return page.density || home.density || 'regular'
|
||||
},
|
||||
pageFrame() {
|
||||
const page = this.currentThemePage || 'home'
|
||||
return this.pageMeta(page).frame || 'none'
|
||||
const page = this.pageMeta(this.currentThemePage || 'home')
|
||||
const home = this.pageMeta('home')
|
||||
return page.frame || home.frame || 'none'
|
||||
},
|
||||
productInfo() {
|
||||
return this.sectionVariant('product', 'info', 'plain')
|
||||
@@ -251,6 +295,12 @@ export default {
|
||||
* 拉主题并按当前路由对齐底栏。
|
||||
* 页面自己写了 onShow 时可能盖掉 mixin 钩子,所以抽成方法让页面显式调用。
|
||||
*/
|
||||
unbindWindowWidth() {
|
||||
if (typeof this._unwatchWindowWidth === 'function') {
|
||||
this._unwatchWindowWidth()
|
||||
this._unwatchWindowWidth = null
|
||||
}
|
||||
},
|
||||
async applyThemeOnShow() {
|
||||
// 先对齐当前页底栏,不要等主题接口,否则邻项互切时 pending 还在栏已经画死
|
||||
syncTabBarByRoute(this)
|
||||
@@ -276,6 +326,17 @@ export default {
|
||||
if (bar && typeof bar.pullTheme === 'function') bar.pullTheme()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this._unwatchWindowWidth = watchWindowWidth((width) => {
|
||||
this.pcWindowWidth = width
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.unbindWindowWidth()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.unbindWindowWidth()
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
},
|
||||
|
||||
19
pages.json
19
pages.json
@@ -104,6 +104,25 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "pkg-more",
|
||||
"name": "more",
|
||||
"pages": [
|
||||
{ "path": "enterprise/detail", "style": { "navigationBarTitleText": "企业详情" } },
|
||||
{ "path": "feedback/index", "style": { "navigationBarTitleText": "用户反馈" } },
|
||||
{ "path": "address/list", "style": { "navigationBarTitleText": "收货地址" } },
|
||||
{ "path": "address/form", "style": { "navigationBarTitleText": "编辑地址" } },
|
||||
{ "path": "favorite/index", "style": { "navigationBarTitleText": "我的收藏" } },
|
||||
{ "path": "after-sale/index", "style": { "navigationBarTitleText": "我的售后" } },
|
||||
{ "path": "after-sale/apply", "style": { "navigationBarTitleText": "申请售后" } },
|
||||
{ "path": "factory/index", "style": { "navigationBarTitleText": "工厂目录" } },
|
||||
{ "path": "factory/detail", "style": { "navigationBarTitleText": "工厂详情" } },
|
||||
{ "path": "colorcard/index", "style": { "navigationBarTitleText": "色卡目录" } },
|
||||
{ "path": "colorcard/detail", "style": { "navigationBarTitleText": "色卡详情" } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
|
||||
@@ -152,6 +152,8 @@
|
||||
</page-container>
|
||||
|
||||
<view class="order-bar" :style="{ background: themeSurface, borderColor: themeBorder }">
|
||||
<button class="quote-btn" open-type="share" :style="{ color: themeText, borderColor: themeBorder }">分享</button>
|
||||
<button class="quote-btn" :style="{ color: themeText, borderColor: themeBorder }" @tap="shareQuote">复制报价</button>
|
||||
<button class="order-btn" :style="{ background: themePrimary, color: themeSurface }" @tap="openOrderForm">
|
||||
{{ Number(info.status) === 1 ? '再次下单' : '生成订单' }}
|
||||
</button>
|
||||
@@ -172,7 +174,10 @@
|
||||
<view class="order-fields">
|
||||
<input class="order-input" v-model="orderForm.receiver_name" placeholder="收件人" />
|
||||
<input class="order-input" v-model="orderForm.receiver_phone" placeholder="联系电话" type="number" />
|
||||
<input class="order-input" v-model="orderForm.receiver_address" placeholder="收货地址" />
|
||||
<view class="addr-pick" @tap="pickAddress">
|
||||
<input class="order-input" v-model="orderForm.receiver_address" placeholder="收货地址" />
|
||||
<text class="addr-link">地址簿</text>
|
||||
</view>
|
||||
<view class="delivery-row">
|
||||
<text
|
||||
v-for="item in deliveryOptions"
|
||||
@@ -196,10 +201,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {deleteCartItemApi, getCartItemApi, updateItemApi} from "@/api/page/cart";
|
||||
import {deleteCartItemApi, exportQuoteApi, getCartItemApi, updateItemApi} from "@/api/page/cart";
|
||||
import {getDefaultAddressApi} from "@/api/page/address";
|
||||
import {createOrderApi} from "@/api/page/order";
|
||||
import {getCache} from "@/utils/cache";
|
||||
import themeMixin from "@/mixins/theme";
|
||||
import { requestOrderSubscribe } from "@/utils/subscribe";
|
||||
import ThemeNavbar from "@/components/theme/ThemeNavbar.vue";
|
||||
|
||||
export default {
|
||||
@@ -249,6 +256,12 @@ export default {
|
||||
this.listId = options.id
|
||||
this.getInfo();
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: this.listName || '清单报价',
|
||||
path: `/pages/cart/detail?id=${this.listId}`,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDelete(index, id) {
|
||||
uni.showModal({
|
||||
@@ -326,10 +339,32 @@ export default {
|
||||
this.orderForm.delivery_type = 1
|
||||
this.orderForm.remark = ''
|
||||
this.orderForm.visible = true
|
||||
getDefaultAddressApi().then((addr) => {
|
||||
if (!addr || !addr.id) return
|
||||
this.orderForm.receiver_name = addr.name || this.orderForm.receiver_name
|
||||
this.orderForm.receiver_phone = addr.phone || this.orderForm.receiver_phone
|
||||
this.orderForm.receiver_address = addr.address || this.orderForm.receiver_address
|
||||
}).catch(() => {})
|
||||
},
|
||||
submitOrder() {
|
||||
pickAddress() {
|
||||
uni.navigateTo({ url: '/pkg-more/address/list?pick=1' })
|
||||
},
|
||||
onPickAddress(item) {
|
||||
if (!item) return
|
||||
this.orderForm.receiver_name = item.name || this.orderForm.receiver_name
|
||||
this.orderForm.receiver_phone = item.phone || this.orderForm.receiver_phone
|
||||
this.orderForm.receiver_address = item.address || this.orderForm.receiver_address
|
||||
},
|
||||
shareQuote() {
|
||||
exportQuoteApi(this.listId).then((res) => {
|
||||
const text = (res && res.text) || this.listName
|
||||
uni.setClipboardData({ data: text })
|
||||
})
|
||||
},
|
||||
async submitOrder() {
|
||||
if (this.orderForm.saving) return
|
||||
this.orderForm.saving = true
|
||||
await requestOrderSubscribe(['pay', 'ship'])
|
||||
createOrderApi(this.listId, {
|
||||
receiver_name: this.orderForm.receiver_name,
|
||||
receiver_phone: this.orderForm.receiver_phone,
|
||||
@@ -651,15 +686,40 @@ view, scroll-view, text, image {
|
||||
padding: 16rpx 30rpx calc(env(safe-area-inset-bottom) + 16rpx);
|
||||
border-top: 1rpx solid;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.quote-btn {
|
||||
flex: 0 0 140rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 26rpx;
|
||||
border: 1rpx solid;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.order-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.addr-pick {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.addr-link {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
top: 18rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-wrap {
|
||||
padding-bottom: 12rpx;
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
'cat-grid--featured': homeCategory === 'featured',
|
||||
'cat-grid--tile': homeCategory === 'tile',
|
||||
}"
|
||||
:style="homeCatGridStyle"
|
||||
>
|
||||
<ThemeCardFrame
|
||||
v-for="(item, index) in categories"
|
||||
@@ -237,7 +238,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else class="pkg-grid" :class="'pkg-grid--' + homePackage">
|
||||
<view v-else class="pkg-grid" :class="'pkg-grid--' + homePackage" :style="homePkgGridStyle">
|
||||
<view
|
||||
v-for="(item, index) in homePackages"
|
||||
:key="item.id"
|
||||
@@ -271,7 +272,11 @@
|
||||
<text class="title-en" :style="{ color: themePrimary }">SELECTION</text>
|
||||
<text class="title-cn" :style="{ color: themeText }">精选型号</text>
|
||||
</view>
|
||||
<view class="home-goods-grid" :class="{ 'is-list': homeProduct === 'list' }">
|
||||
<view
|
||||
class="home-goods-grid"
|
||||
:class="{ 'is-list': homeProduct === 'list' }"
|
||||
:style="homeGoodsGridStyle"
|
||||
>
|
||||
<view
|
||||
v-for="item in homeGoods"
|
||||
:key="item.id"
|
||||
@@ -346,6 +351,27 @@ export default {
|
||||
paddingBottom: 'calc(60px + env(safe-area-inset-bottom))',
|
||||
}
|
||||
},
|
||||
/** 宽屏把精选型号从双列扩开,卡片等比例变窄 */
|
||||
homeGoodsGridStyle() {
|
||||
if (this.homeProduct === 'list') return {}
|
||||
return {
|
||||
gridTemplateColumns: 'repeat(' + this.pcGoodsCols + ', 1fr)',
|
||||
}
|
||||
},
|
||||
/** 分类宫格:mosaic / featured 有自己的非对称列,不要覆盖 */
|
||||
homeCatGridStyle() {
|
||||
if (this.homeCategory === 'mosaic' || this.homeCategory === 'featured') return {}
|
||||
return {
|
||||
gridTemplateColumns: 'repeat(' + this.pcCatCols + ', 1fr)',
|
||||
}
|
||||
},
|
||||
/** 套餐宫格:杂志单列保持原样,其余跟商品列数走 */
|
||||
homePkgGridStyle() {
|
||||
if (this.homePackage === 'magazine') return {}
|
||||
return {
|
||||
gridTemplateColumns: 'repeat(' + this.pcGoodsCols + ', 1fr)',
|
||||
}
|
||||
},
|
||||
heroWrapStyle() {
|
||||
const order = { order: this.sectionOrder('home', 'hero') }
|
||||
if (this.homeHero === 'fullscreen') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="login-container" :style="themePageStyle">
|
||||
<ThemeNavbar title="登录" />
|
||||
<view class="logo-area">
|
||||
<image
|
||||
@@ -7,7 +7,7 @@
|
||||
mode="aspectFill"
|
||||
class="logo-image"
|
||||
/>
|
||||
<text class="logo-text">佛山家具工厂</text>
|
||||
<text class="logo-text">{{ brandName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="login-form">
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<script>
|
||||
import {loginApi} from "@/api/page/auth";
|
||||
import { BRAND_NAME } from "@/api/env";
|
||||
import {getCache, setCache} from "@/utils/cache";
|
||||
import ThemeNavbar from "@/components/theme/ThemeNavbar.vue";
|
||||
import themeMixin from "@/mixins/theme";
|
||||
@@ -38,6 +39,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
phoneCode: '',
|
||||
brandName: BRAND_NAME,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ThemeCardFrame class="user-card-wrap">
|
||||
<view class="user-card animate-slide-down" :style="{ background: themeSurface }">
|
||||
<view class="user-info-row" @click="handleUserClick">
|
||||
<view class="avatar-wrapper" :style="{ boxShadow: '0 0 0 4rpx ' + themePrimary }">
|
||||
<view class="avatar-wrapper" :style="{ borderColor: themePrimary }">
|
||||
<image
|
||||
:src="userInfo.avatar || 'https://img2.baidu.com/it/u=2953585264,744730101&fm=253&fmt=auto&app=138&f=JPEG?w=360&h=360'"
|
||||
mode="aspectFill"
|
||||
@@ -27,19 +27,16 @@
|
||||
<view class="subtitle" v-else :style="{ color: themeTextSoft }">点击登录开启家具选购之旅</view>
|
||||
</view>
|
||||
<view class="arrow-icon">
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="16"></u-icon>
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="14"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<view class="num" :style="{ color: themePrimary }">{{ count }}</view>
|
||||
<view class="label" :style="{ color: themeTextSoft }">当前清单数量</view>
|
||||
<view class="stats-row" :style="{ borderTopColor: themeBorder }">
|
||||
<view class="stat-copy">
|
||||
<text class="num" :style="{ color: themePrimary }">{{ count }}</text>
|
||||
<text class="label" :style="{ color: themeTextSoft }">当前清单</text>
|
||||
</view>
|
||||
<view class="vertical-line" :style="{ background: themeBorder }"></view>
|
||||
<view class="stat-item" @click="goCart">
|
||||
<view class="action-text" :style="{ background: themeText, color: themeSurface }">去查看</view>
|
||||
<view class="action-text" :style="{ background: themePrimary, color: themeSurface }" @click.stop="goCart">
|
||||
去查看
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -55,40 +52,40 @@
|
||||
<u-icon name="arrow-right" color="rgba(255,255,255,0.6)" size="14"></u-icon>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<!-- 宫格三列收紧;列表按交易 / 账户 / 更多分组,避免九个入口摊成一块空白板 -->
|
||||
<view
|
||||
v-for="group in menuGroups"
|
||||
:key="group.key"
|
||||
class="menu-group animate-fade-in"
|
||||
:class="'menu-group--' + mineMenu"
|
||||
:style="{ background: themeSurface }"
|
||||
>
|
||||
<view class="menu-item" @click="goCart">
|
||||
<text
|
||||
v-if="!mineIsGrid && group.title"
|
||||
class="menu-group-title"
|
||||
:style="{ color: themeTextSoft }"
|
||||
>{{ group.title }}</text>
|
||||
<view
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
class="menu-item"
|
||||
@click="onMenu(item)"
|
||||
>
|
||||
<view class="item-left">
|
||||
<ThemeIcon src="/static/icon/await.png" :size="20" />
|
||||
<text :style="{ color: themeText, marginLeft: '20rpx' }">我的清单</text>
|
||||
<view class="icon-well" :style="iconWellStyle">
|
||||
<ThemeIcon :name="item.icon" :size="mineIsGrid ? 20 : 18" />
|
||||
</view>
|
||||
<text class="menu-label" :style="{ color: themeText }">{{ item.label }}</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="14"></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="goOrder">
|
||||
<view class="item-left">
|
||||
<ThemeIcon name="file-text" :size="20" />
|
||||
<text :style="{ color: themeText, marginLeft: '20rpx' }">我的订单</text>
|
||||
<view v-if="!mineIsGrid" class="menu-arrow">
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="12"></u-icon>
|
||||
</view>
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="14"></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="goMy">
|
||||
<view class="item-left">
|
||||
<ThemeIcon name="setting-fill" :size="20" />
|
||||
<text :style="{ color: themeText, marginLeft: '20rpx' }">个人设置</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" :color="themeTextSoft" size="14"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<Footer />
|
||||
</view>
|
||||
<ThemeTabBar id="themeTabBar" ref="themeTabBar" />
|
||||
<ThemeTabBar id="themeTabBar" ref="themeTabBar" :placeholder="false" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -110,37 +107,45 @@ export default {
|
||||
currentThemePage: 'mine',
|
||||
userInfo: {},
|
||||
count: 0,
|
||||
indexList: [
|
||||
// {
|
||||
// name: '用户反馈',
|
||||
// icon: {
|
||||
// color: '#ff8800',
|
||||
// size: '26',
|
||||
// type: 'info-circle'
|
||||
// },
|
||||
// page: '/pages/feedback/feedback'
|
||||
// },
|
||||
// {
|
||||
// name: '我的邮件',
|
||||
// icon: {
|
||||
// color: '#ff8800',
|
||||
// size: '26',
|
||||
// type: 'email'
|
||||
// },
|
||||
// page: '/pages/email/email'
|
||||
// },
|
||||
// {
|
||||
// name: '分享有礼',
|
||||
// icon: {
|
||||
// color: '#ff8800',
|
||||
// size: '26',
|
||||
// type: 'gift'
|
||||
// },
|
||||
// page: '/pages/share/share'
|
||||
// },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 宫格 / 瓷砖 / 卡片:三列图标井,不画列表箭头 */
|
||||
mineIsGrid() {
|
||||
return ['grid', 'tile', 'card'].includes(this.mineMenu)
|
||||
},
|
||||
iconWellStyle() {
|
||||
return {
|
||||
background: this.themeVars['--color-primary-soft'] || 'rgba(176, 141, 87, 0.14)',
|
||||
}
|
||||
},
|
||||
/** 九个入口:宫格合成一块 3×3,列表拆成三组避免一长条 */
|
||||
menuGroups() {
|
||||
const trade = [
|
||||
{ key: 'cart', label: '我的清单', icon: 'list', action: 'cart' },
|
||||
{ key: 'order', label: '我的订单', icon: 'file-text', action: 'order' },
|
||||
{ key: 'after', label: '我的售后', icon: 'rmb-circle', url: '/pkg-more/after-sale/index' },
|
||||
]
|
||||
const account = [
|
||||
{ key: 'setting', label: '个人设置', icon: 'setting-fill', action: 'setting' },
|
||||
{ key: 'fav', label: '我的收藏', icon: 'star', url: '/pkg-more/favorite/index' },
|
||||
{ key: 'addr', label: '收货地址', icon: 'map', url: '/pkg-more/address/list' },
|
||||
]
|
||||
const more = [
|
||||
{ key: 'fb', label: '用户反馈', icon: 'chat', url: '/pkg-more/feedback/index' },
|
||||
{ key: 'factory', label: '工厂目录', icon: 'home', url: '/pkg-more/factory/index', guest: true },
|
||||
{ key: 'color', label: '色卡目录', icon: 'grid', url: '/pkg-more/colorcard/index', guest: true },
|
||||
]
|
||||
if (this.mineIsGrid) {
|
||||
return [{ key: 'all', title: '', items: trade.concat(account, more) }]
|
||||
}
|
||||
return [
|
||||
{ key: 'trade', title: '交易', items: trade },
|
||||
{ key: 'account', title: '账户', items: account },
|
||||
{ key: 'more', title: '更多', items: more },
|
||||
]
|
||||
},
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow();
|
||||
this.checkLoginAndLoad();
|
||||
@@ -184,8 +189,33 @@ export default {
|
||||
uni.navigateTo({ url: '/pages/order/list' });
|
||||
},
|
||||
goEnterpriseDetail() {
|
||||
// 企业详情跳转逻辑
|
||||
}
|
||||
uni.navigateTo({ url: '/pkg-more/enterprise/detail' })
|
||||
},
|
||||
goPage(url) {
|
||||
if (!this.userInfo.id && url.indexOf('factory') < 0 && url.indexOf('colorcard') < 0) {
|
||||
this.loginFun()
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
/** 宫格和列表共用入口,避免九份重复点击 */
|
||||
onMenu(item) {
|
||||
if (item.action === 'cart') {
|
||||
this.goCart()
|
||||
return
|
||||
}
|
||||
if (item.action === 'order') {
|
||||
this.goOrder()
|
||||
return
|
||||
}
|
||||
if (item.action === 'setting') {
|
||||
this.goMy()
|
||||
return
|
||||
}
|
||||
if (item.url) {
|
||||
this.goPage(item.url)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -195,6 +225,8 @@ export default {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F7FA;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bg {
|
||||
@@ -211,50 +243,63 @@ export default {
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 140rpx 30rpx 40rpx;
|
||||
flex: 1;
|
||||
/* 底栏是 fixed,内容区自己留出 pill + 安全区,避免最后一行被挡住 */
|
||||
padding: 140rpx 30rpx calc(48rpx + 60px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 间距写在 ThemeCardFrame 根上,写在内层会被外壳吃掉,两张大卡会贴死 */
|
||||
.user-card-wrap {
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* User Card */
|
||||
.user-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(180, 133, 77, 0.08);
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.user-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-right: 24rpx;
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
margin-bottom: 6rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
@@ -262,38 +307,33 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 30rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
margin-top: 22rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
.stat-copy {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.vertical-line {
|
||||
width: 2rpx;
|
||||
height: 40rpx;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
display: inline-block;
|
||||
padding: 12rpx 32rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 30rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
/* Enterprise Card */
|
||||
@@ -335,61 +375,115 @@ export default {
|
||||
.menu-group {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 10rpx 0;
|
||||
padding: 8rpx 0;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.menu-group--grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.menu-group--grid .menu-item {
|
||||
width: 50%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.menu-group--grid .menu-item .item-left {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
.menu-group-title {
|
||||
display: block;
|
||||
padding: 16rpx 28rpx 4rpx;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx 40rpx;
|
||||
transition: background 0.2s;
|
||||
padding: 22rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: #f9f9f9;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
.icon-well {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.u-icon-fix {
|
||||
width: 40rpx;
|
||||
.menu-label {
|
||||
margin-left: 18rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 三列用 grid 留出行列间距,33% flex 会把 gutter 吃掉 */
|
||||
.menu-group--grid,
|
||||
.menu-group--tile,
|
||||
.menu-group--card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8rpx 12rpx;
|
||||
padding: 24rpx 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.menu-group--grid .menu-item,
|
||||
.menu-group--tile .menu-item,
|
||||
.menu-group--card .menu-item {
|
||||
width: auto;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 16rpx 4rpx 12rpx;
|
||||
}
|
||||
|
||||
.menu-group--grid .item-left,
|
||||
.menu-group--tile .item-left,
|
||||
.menu-group--card .item-left {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-group--grid .icon-well,
|
||||
.menu-group--tile .icon-well,
|
||||
.menu-group--card .icon-well {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.menu-group--grid .menu-label,
|
||||
.menu-group--tile .menu-label,
|
||||
.menu-group--card .menu-label {
|
||||
margin-left: 0;
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-group--tile .menu-item {
|
||||
padding: 12rpx 6rpx 16rpx;
|
||||
}
|
||||
|
||||
.menu-group--card .menu-item {
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.menu-group--card .item-left {
|
||||
width: 100%;
|
||||
padding: 16rpx 4rpx 18rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.menu-group--compact .menu-item {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-20rpx); }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="detail-page" :style="themePageStyle">
|
||||
<ThemeNavbar title="订单详情" />
|
||||
<ThemeNavbar v-if="!voucherVisible" title="订单详情" />
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading && !order.id" class="empty-tip">加载中...</view>
|
||||
<view v-else-if="!order.id" class="empty-tip">订单不存在</view>
|
||||
@@ -127,6 +127,13 @@
|
||||
>
|
||||
确认收货
|
||||
</view>
|
||||
<view
|
||||
class="action-btn outline-btn"
|
||||
v-if="canAfterSale"
|
||||
@tap="onAfterSale"
|
||||
>
|
||||
申请售后
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传凭证 page-container(防意外退出,按规则用 v-if) -->
|
||||
@@ -136,10 +143,13 @@
|
||||
:overlay="true"
|
||||
:close-on-overlay="false"
|
||||
position="right"
|
||||
title="上传转账凭证"
|
||||
@clickoverlay="closeVoucher"
|
||||
>
|
||||
<view class="voucher-panel">
|
||||
<view class="voucher-panel" :style="{ paddingTop: voucherPadTop + 'px' }">
|
||||
<view class="voucher-head">
|
||||
<text class="voucher-head-title">上传转账凭证</text>
|
||||
<text class="voucher-head-close" @tap="closeVoucher">关闭</text>
|
||||
</view>
|
||||
<view class="voucher-tip">
|
||||
请上传银行/微信转账截图,后台审核通过后订单状态自动推进。
|
||||
</view>
|
||||
@@ -196,6 +206,7 @@ import {
|
||||
} from '@/api/page/order.js'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import { requestOrderSubscribe } from '@/utils/subscribe'
|
||||
|
||||
const STATUS_MAP = {
|
||||
0: '待付款',
|
||||
@@ -223,7 +234,8 @@ export default {
|
||||
voucherVisible: false,
|
||||
voucherImages: [],
|
||||
voucherAmount: '',
|
||||
submitting: false
|
||||
submitting: false,
|
||||
statusBarHeight: 20
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -240,8 +252,11 @@ export default {
|
||||
canConfirm() {
|
||||
return this.order.status === 2
|
||||
},
|
||||
canAfterSale() {
|
||||
return this.order.status === 1 || this.order.status === 2 || this.order.status === 3
|
||||
},
|
||||
showActions() {
|
||||
return this.order.id && (this.canCancel || this.canPay || this.canUploadVoucher || this.canConfirm)
|
||||
return this.order.id && (this.canCancel || this.canPay || this.canUploadVoucher || this.canConfirm || this.canAfterSale)
|
||||
},
|
||||
timeline() {
|
||||
if (!this.order.id) return []
|
||||
@@ -266,10 +281,20 @@ export default {
|
||||
nodes.push({title: '已取消', time: '', type: 'cancel'})
|
||||
}
|
||||
return nodes
|
||||
},
|
||||
/** page-container 没有系统导航,内容要从状态栏下面开始 */
|
||||
voucherPadTop() {
|
||||
return this.statusBarHeight
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.orderId = Number(options.id || 0)
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
this.statusBarHeight = sys.statusBarHeight || 20
|
||||
} catch (e) {
|
||||
this.statusBarHeight = 20
|
||||
}
|
||||
this.fetchDetail()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
@@ -341,7 +366,12 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
onAfterSale() {
|
||||
uni.navigateTo({ url: `/pkg-more/after-sale/apply?order_id=${this.orderId}` })
|
||||
},
|
||||
async onPay() {
|
||||
// 支付手势里同时要发货模板,否则后台发货时订阅发不出去
|
||||
await requestOrderSubscribe(['pay', 'ship'])
|
||||
uni.showLoading({title: '正在调起支付...'})
|
||||
try {
|
||||
const params = await wechatPayApi(this.orderId)
|
||||
@@ -444,10 +474,12 @@ export default {
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 140rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 48rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@@ -685,17 +717,36 @@ export default {
|
||||
|
||||
/* 凭证上传 page-container 内容 */
|
||||
.voucher-panel {
|
||||
padding: 32rpx;
|
||||
padding: 0 32rpx 180rpx;
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.voucher-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.voucher-head-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.voucher-head-close {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.voucher-tip {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 32rpx;
|
||||
margin: 16rpx 0 32rpx;
|
||||
}
|
||||
|
||||
.voucher-images {
|
||||
|
||||
@@ -1,74 +1,96 @@
|
||||
<template>
|
||||
<view class="order-page" :style="themePageStyle">
|
||||
<ThemeNavbar title="我的订单" :show-back="false" />
|
||||
<!-- 顶部状态 Tab -->
|
||||
<!-- 顶部状态 Tab:点击与左右滑共用 currentIndex -->
|
||||
<view class="status-tabs" :style="{ background: themeSurface, borderColor: themeBorder }">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.key"
|
||||
class="status-tab"
|
||||
:class="{active: currentStatus === tab.value}"
|
||||
:style="{ color: currentStatus === tab.value ? themePrimary : themeTextSoft }"
|
||||
@tap="switchTab(tab.value)"
|
||||
:class="{active: currentIndex === index}"
|
||||
:style="{ color: currentIndex === index ? themePrimary : themeTextSoft }"
|
||||
@tap="onTabTap(index)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<scroll-view
|
||||
class="order-list"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
<swiper
|
||||
class="order-swiper"
|
||||
:style="{ height: swiperHeight + 'px' }"
|
||||
:current="currentIndex"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<view v-if="loading && list.length === 0" class="empty-tip" :style="{ color: themeTextSoft }">加载中...</view>
|
||||
<view v-else-if="list.length === 0" class="empty-tip" :style="{ color: themeTextSoft }">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text>暂无订单</text>
|
||||
</view>
|
||||
<swiper-item v-for="(tab, index) in tabs" :key="tab.key">
|
||||
<scroll-view
|
||||
class="order-list"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore(index)"
|
||||
>
|
||||
<view
|
||||
v-if="tabCaches[index] && tabCaches[index].loading && tabCaches[index].list.length === 0"
|
||||
class="empty-tip"
|
||||
:style="{ color: themeTextSoft }"
|
||||
>加载中...</view>
|
||||
<view
|
||||
v-else-if="tabCaches[index] && tabCaches[index].list.length === 0"
|
||||
class="empty-tip"
|
||||
:style="{ color: themeTextSoft }"
|
||||
>
|
||||
<text class="empty-icon">📭</text>
|
||||
<text>暂无订单</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="order in list"
|
||||
:key="order.id"
|
||||
class="order-card"
|
||||
:style="{ background: themeSurface, borderColor: themeBorder }"
|
||||
@tap="goDetail(order.id)"
|
||||
>
|
||||
<view class="order-card-header">
|
||||
<text class="order-no" :style="{ color: themeTextSoft }">订单号:{{ order.order_no }}</text>
|
||||
<text class="order-status" :style="{ color: statusColor(order.status) }">
|
||||
{{ statusText(order.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="order in (tabCaches[index] && tabCaches[index].list) || []"
|
||||
:key="order.id"
|
||||
class="order-card"
|
||||
:style="{ background: themeSurface, borderColor: themeBorder }"
|
||||
@tap="goDetail(order.id)"
|
||||
>
|
||||
<view class="order-card-header">
|
||||
<text class="order-no" :style="{ color: themeTextSoft }">订单号:{{ order.order_no }}</text>
|
||||
<text class="order-status" :style="{ color: statusColor(order.status) }">
|
||||
{{ statusText(order.status) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="order-card-body">
|
||||
<view class="order-item-thumb" v-if="order.items && order.items.length">
|
||||
<image
|
||||
v-for="(item, idx) in order.items.slice(0, 4)"
|
||||
:key="idx"
|
||||
:src="item.cover"
|
||||
mode="aspectFill"
|
||||
class="thumb"
|
||||
/>
|
||||
<view v-if="order.items.length > 4" class="thumb-more">
|
||||
+{{ order.items.length - 4 }}
|
||||
<view class="order-card-body">
|
||||
<view class="order-item-thumb" v-if="order.items && order.items.length">
|
||||
<image
|
||||
v-for="(item, idx) in order.items.slice(0, 4)"
|
||||
:key="idx"
|
||||
:src="item.cover"
|
||||
mode="aspectFill"
|
||||
class="thumb"
|
||||
/>
|
||||
<view v-if="order.items.length > 4" class="thumb-more">
|
||||
+{{ order.items.length - 4 }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="order-item-empty">共 {{ order.item_count || 0 }} 件商品</view>
|
||||
</view>
|
||||
|
||||
<view class="order-card-footer" :style="{ borderColor: themeBorder }">
|
||||
<view class="order-amount">
|
||||
<text class="amount-label" :style="{ color: themeTextSoft }">合计</text>
|
||||
<text class="amount-value" :style="{ color: themePrice }">¥{{ order.total_amount_text || '0' }}</text>
|
||||
</view>
|
||||
<view class="order-time" :style="{ color: themeTextSoft }">{{ order.created_at }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="order-item-empty">共 {{ order.item_count || 0 }} 件商品</view>
|
||||
</view>
|
||||
|
||||
<view class="order-card-footer" :style="{ borderColor: themeBorder }">
|
||||
<view class="order-amount">
|
||||
<text class="amount-label" :style="{ color: themeTextSoft }">合计</text>
|
||||
<text class="amount-value" :style="{ color: themePrice }">¥{{ order.total_amount_text || '0' }}</text>
|
||||
</view>
|
||||
<view class="order-time" :style="{ color: themeTextSoft }">{{ order.created_at }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && list.length > 0" class="load-more-tip">加载中...</view>
|
||||
<view v-if="noMore && list.length > 0" class="load-more-tip">没有更多了</view>
|
||||
</scroll-view>
|
||||
<view
|
||||
v-if="tabCaches[index] && tabCaches[index].loading && tabCaches[index].list.length > 0"
|
||||
class="load-more-tip"
|
||||
>加载中...</view>
|
||||
<view
|
||||
v-if="tabCaches[index] && tabCaches[index].noMore && tabCaches[index].list.length > 0"
|
||||
class="load-more-tip"
|
||||
>没有更多了</view>
|
||||
</scroll-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<ThemeTabBar id="themeTabBar" ref="themeTabBar" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -91,40 +113,64 @@ const STATUS_MAP = {
|
||||
4: '已取消'
|
||||
}
|
||||
|
||||
function emptyCache() {
|
||||
return {list: [], page: 1, noMore: false, loaded: false, loading: false}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar, ThemeTabBar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return {
|
||||
// 顶部 Tab:undefined 表示全部
|
||||
tabs: [
|
||||
{label: '全部', value: undefined},
|
||||
{label: '待付款', value: 0},
|
||||
{label: '待发货', value: 1},
|
||||
{label: '待收货', value: 2},
|
||||
{label: '已完成', value: 3}
|
||||
{label: '全部', value: undefined, key: 'all'},
|
||||
{label: '待付款', value: 0, key: '0'},
|
||||
{label: '待发货', value: 1, key: '1'},
|
||||
{label: '待收货', value: 2, key: '2'},
|
||||
{label: '已完成', value: 3, key: '3'}
|
||||
],
|
||||
currentStatus: undefined,
|
||||
list: [],
|
||||
page: 1,
|
||||
currentIndex: 0,
|
||||
tabCaches: [],
|
||||
pageSize: 20,
|
||||
loading: false,
|
||||
noMore: false
|
||||
swiperHeight: 400
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 支持从外部带 status 跳进来(如「待付款」入口)
|
||||
this.tabCaches = this.tabs.map(() => emptyCache())
|
||||
if (options.status !== undefined && options.status !== '') {
|
||||
this.currentStatus = Number(options.status)
|
||||
const idx = this.tabs.findIndex((tab) => Number(tab.value) === Number(options.status))
|
||||
if (idx >= 0) this.currentIndex = idx
|
||||
}
|
||||
this.fetchList(true)
|
||||
this.measureSwiper()
|
||||
this.ensureTab(this.currentIndex, true)
|
||||
},
|
||||
onReady() {
|
||||
this.measureSwiper()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.fetchList(true).finally(() => {
|
||||
this.ensureTab(this.currentIndex, true).finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* swiper 必须有明确像素高度,否则微信里内容高度为 0
|
||||
*/
|
||||
measureSwiper() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
const status = sys.statusBarHeight || 20
|
||||
const menu = uni.getMenuButtonBoundingClientRect && uni.getMenuButtonBoundingClientRect()
|
||||
const inner = menu && menu.height ? (menu.top - status) * 2 + menu.height : 44
|
||||
const navH = this.isPcWeixin ? 0 : (status + inner)
|
||||
const tabH = 44
|
||||
const safeBottom = (sys.safeAreaInsets && sys.safeAreaInsets.bottom) || 0
|
||||
const tabbarH = 60 + safeBottom
|
||||
this.swiperHeight = Math.max(240, (sys.windowHeight || 667) - navH - tabH - tabbarH)
|
||||
} catch (e) {
|
||||
this.swiperHeight = 400
|
||||
}
|
||||
},
|
||||
statusText(status) {
|
||||
return STATUS_MAP[status] || '-'
|
||||
},
|
||||
@@ -133,44 +179,59 @@ export default {
|
||||
if (status === 4) return this.themeTextSoft
|
||||
return this.themePrimary
|
||||
},
|
||||
switchTab(value) {
|
||||
if (this.currentStatus === value) return
|
||||
this.currentStatus = value
|
||||
this.fetchList(true)
|
||||
onTabTap(index) {
|
||||
if (this.currentIndex === index) return
|
||||
this.currentIndex = index
|
||||
this.ensureTab(index)
|
||||
},
|
||||
async fetchList(reset = false) {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
onSwiperChange(e) {
|
||||
const index = (e.detail && e.detail.current) || 0
|
||||
this.currentIndex = index
|
||||
this.ensureTab(index)
|
||||
},
|
||||
/**
|
||||
* 每个 Tab 自己缓存列表,滑到没加载过的才请求,避免来回清空
|
||||
*/
|
||||
ensureTab(index, force = false) {
|
||||
const cache = this.tabCaches[index]
|
||||
if (!cache) return Promise.resolve()
|
||||
if (!force && (cache.loaded || cache.loading)) return Promise.resolve()
|
||||
return this.fetchList(index, true)
|
||||
},
|
||||
async fetchList(index, reset = false) {
|
||||
const cache = this.tabCaches[index]
|
||||
if (!cache || cache.loading) return
|
||||
cache.loading = true
|
||||
this.$forceUpdate()
|
||||
if (reset) {
|
||||
this.page = 1
|
||||
this.noMore = false
|
||||
cache.page = 1
|
||||
cache.noMore = false
|
||||
}
|
||||
try {
|
||||
const tab = this.tabs[index]
|
||||
const res = await getOrderListApi({
|
||||
status: this.currentStatus,
|
||||
page: this.page,
|
||||
status: tab.value,
|
||||
page: cache.page,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
// 后端返回 { items, total } 或数组(兼容两种结构)
|
||||
const items = Array.isArray(res) ? res : (res.items || res.list || [])
|
||||
if (reset) {
|
||||
this.list = items
|
||||
} else {
|
||||
this.list = this.list.concat(items)
|
||||
}
|
||||
cache.list = reset ? items : cache.list.concat(items)
|
||||
cache.loaded = true
|
||||
if (items.length < this.pageSize) {
|
||||
this.noMore = true
|
||||
cache.noMore = true
|
||||
}
|
||||
} catch (e) {
|
||||
// 拦截器已统一 toast,这里只兜底
|
||||
// 拦截器已统一 toast
|
||||
} finally {
|
||||
this.loading = false
|
||||
cache.loading = false
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
loadMore() {
|
||||
if (this.noMore || this.loading) return
|
||||
this.page += 1
|
||||
this.fetchList(false)
|
||||
loadMore(index) {
|
||||
const cache = this.tabCaches[index]
|
||||
if (!cache || cache.noMore || cache.loading) return
|
||||
cache.page += 1
|
||||
this.fetchList(index, false)
|
||||
},
|
||||
goDetail(id) {
|
||||
uni.navigateTo({url: `/pages/order/detail?id=${id}`})
|
||||
@@ -189,8 +250,7 @@ export default {
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -206,8 +266,12 @@ export default {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.order-swiper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -237,26 +301,6 @@ export default {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-0 {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.status-1 {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.status-2 {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.status-3 {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.status-4 {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-card-body {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
@@ -165,12 +165,24 @@ export default {
|
||||
lists: [],
|
||||
pickerVisible: false,
|
||||
saving: false,
|
||||
// 分享落地时详情请求可能早于静默登录,登录后再拉一次才能写 pid
|
||||
needRebind: false,
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.id = Number(options.id || 0)
|
||||
this.p_user_id = Number(options.p_user_id || 0)
|
||||
this.needRebind = !!(this.p_user_id && !getCache('token'))
|
||||
this.loadDetail()
|
||||
if (this.needRebind) {
|
||||
uni.$on('wx-login-ready', this.onWxLoginReady)
|
||||
if (getCache('token')) {
|
||||
this.onWxLoginReady()
|
||||
}
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('wx-login-ready', this.onWxLoginReady)
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
@@ -192,6 +204,15 @@ export default {
|
||||
this.info = res || { items: [] }
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 静默登录完成后补绑:首次详情请求没带上 token 时 inheritAgentPrice 不会写库
|
||||
*/
|
||||
onWxLoginReady() {
|
||||
if (!this.needRebind) return
|
||||
this.needRebind = false
|
||||
uni.$off('wx-login-ready', this.onWxLoginReady)
|
||||
this.loadDetail()
|
||||
},
|
||||
isBoundOther(list) {
|
||||
const bound = Number(list.package_id || 0)
|
||||
return bound > 0 && bound !== Number(this.id)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<ThemeSearch
|
||||
v-model="keyword"
|
||||
placeholder="搜索套餐名称"
|
||||
:variant="homeSearch"
|
||||
:variant="packageSearch"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
@@ -34,26 +34,29 @@
|
||||
|
||||
<ThemeEnter v-else>
|
||||
<view class="pkg-list" :class="'pkg-list--' + packageList">
|
||||
<ThemeCardFrame
|
||||
<!-- pkg-card 放在本页节点上,避免 scoped 打不到 ThemeCardFrame 根节点 -->
|
||||
<view
|
||||
v-for="(item, index) in items"
|
||||
:key="item.id"
|
||||
class="pkg-card"
|
||||
>
|
||||
<view class="pkg-inner" @tap="goDetail(item.id)">
|
||||
<image :src="item.cover" mode="aspectFill" class="pkg-cover" />
|
||||
<view class="pkg-veil"></view>
|
||||
<view class="pkg-meta">
|
||||
<text class="pkg-vol" :style="{ color: themePrimary }">SET {{ padNo(index + 1) }}</text>
|
||||
<text class="pkg-name">{{ item.name }}</text>
|
||||
<text v-if="item.subtitle" class="pkg-sub">{{ item.subtitle }}</text>
|
||||
<text class="pkg-count">{{ item.item_count || 0 }} 件搭配</text>
|
||||
<view v-if="item.is_show_price" class="pkg-price">
|
||||
<text class="pkg-origin">¥{{ item.original_amount_text }}</text>
|
||||
<text class="pkg-quote" :style="{ color: themePrice }">¥{{ item.package_amount_text }}</text>
|
||||
<ThemeCardFrame>
|
||||
<view class="pkg-inner" @tap="goDetail(item.id)">
|
||||
<image :src="item.cover" mode="aspectFill" class="pkg-cover" />
|
||||
<view class="pkg-veil"></view>
|
||||
<view class="pkg-meta">
|
||||
<text class="pkg-vol" :style="{ color: themePrimary }">SET {{ padNo(index + 1) }}</text>
|
||||
<text class="pkg-name">{{ item.name }}</text>
|
||||
<text v-if="item.subtitle" class="pkg-sub">{{ item.subtitle }}</text>
|
||||
<text class="pkg-count">{{ item.item_count || 0 }} 件搭配</text>
|
||||
<view v-if="item.is_show_price" class="pkg-price">
|
||||
<text class="pkg-origin">¥{{ item.original_amount_text }}</text>
|
||||
<text class="pkg-quote" :style="{ color: themePrice }">¥{{ item.package_amount_text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</ThemeCardFrame>
|
||||
</ThemeCardFrame>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingMore" class="loading-more">
|
||||
<ThemeSpinner text="加载更多" />
|
||||
@@ -189,6 +192,19 @@ export default {
|
||||
.content-body {
|
||||
flex: 1;
|
||||
}
|
||||
.skin--magazine .page-en {
|
||||
letter-spacing: 12rpx;
|
||||
}
|
||||
.skin--magazine .page-cn {
|
||||
font-weight: 400;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
.density--compact .pkg-list {
|
||||
gap: 16rpx;
|
||||
}
|
||||
.density--airy .pkg-list {
|
||||
gap: 40rpx;
|
||||
}
|
||||
.page-head {
|
||||
padding: 12rpx 32rpx 8rpx;
|
||||
}
|
||||
@@ -302,9 +318,14 @@ export default {
|
||||
font-weight: 500;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
/* 首图:第一张拉高,其余略矮,和装修「首图」对得上 */
|
||||
.pkg-list--featured .pkg-card:first-child .pkg-cover {
|
||||
height: 520rpx;
|
||||
height: 560rpx;
|
||||
}
|
||||
.pkg-list--featured .pkg-card:not(:first-child) .pkg-cover {
|
||||
height: 320rpx;
|
||||
}
|
||||
/* 刊名条:左图右文,取消压字,换主题立刻能看出差别 */
|
||||
.pkg-list--magazine .pkg-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -328,6 +349,7 @@ export default {
|
||||
.pkg-list--magazine .pkg-origin {
|
||||
color: inherit;
|
||||
}
|
||||
/* 单列:更紧凑的左图右文 */
|
||||
.pkg-list--list .pkg-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
<template>
|
||||
<view class="container" :style="themePageStyle">
|
||||
<ThemeNavbar title="商品详情" />
|
||||
<ThemeNavbar title="商品详情">
|
||||
<template #right>
|
||||
<view class="nav-actions">
|
||||
<button class="nav-icon-btn" open-type="share" hover-class="none" :style="{ background: themeSurface, color: themeText }">
|
||||
<u-icon name="share-fill" :color="themeText" size="18"></u-icon>
|
||||
</button>
|
||||
<view class="nav-icon-btn" :style="{ background: themeSurface }" @tap="toggleFav">
|
||||
<u-icon :name="product.favorited ? 'star-fill' : 'star'" :color="product.favorited ? themePrimary : themeText" size="18"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</ThemeNavbar>
|
||||
<view v-if="isPcWeixin" class="pc-actions">
|
||||
<button class="nav-icon-btn" open-type="share" hover-class="none" :style="{ background: themeSurface, color: themeText }">
|
||||
<u-icon name="share-fill" :color="themeText" size="18"></u-icon>
|
||||
</button>
|
||||
<view class="nav-icon-btn" :style="{ background: themeSurface }" @tap="toggleFav">
|
||||
<u-icon :name="product.favorited ? 'star-fill' : 'star'" :color="product.favorited ? themePrimary : themeText" size="18"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="swiper-container"
|
||||
:class="'gallery--' + productGallery"
|
||||
@@ -62,9 +81,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="share-fixed-btn animate-scale" open-type="share" hover-class="none" :style="{ background: themeSurface, color: themeText }">
|
||||
<u-icon name="share-fill" :color="themeText" size="20"></u-icon>
|
||||
</button>
|
||||
<video
|
||||
v-if="product.video"
|
||||
:src="product.video"
|
||||
class="product-video"
|
||||
controls
|
||||
:style="{ background: themeSurface }"
|
||||
/>
|
||||
|
||||
<ThemeEnter>
|
||||
<view class="info-card animate-slide-up" :class="'info--' + productInfo" :style="{ background: themeSurface, borderRadius: themeRadiusLg }">
|
||||
@@ -240,6 +263,7 @@
|
||||
<script>
|
||||
import { getProductDetailApi } from "@/api/page/product";
|
||||
import { getCartListApi, toCartApi } from "@/api/page/cart";
|
||||
import { toggleFavoriteApi } from "@/api/page/favorite";
|
||||
import {getCache} from "@/utils/cache";
|
||||
import { urlSafeBase64Encode } from "@/utils/utils";
|
||||
import themeMixin from "@/mixins/theme";
|
||||
@@ -259,7 +283,9 @@ export default {
|
||||
listOption: [],
|
||||
galleryIndex: 0,
|
||||
specPicker: { visible: false, listId: 0, sheets: [], priceSheetId: 0, quantity: 1 },
|
||||
product: { title: '', cover: '', price_sheet: [], alias: '', carousel: [] }
|
||||
product: { title: '', cover: '', price_sheet: [], alias: '', carousel: [], video: '', favorited: false },
|
||||
// 分享落地时详情请求可能早于静默登录,登录后再拉一次才能写 pid
|
||||
needRebind: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -289,7 +315,18 @@ export default {
|
||||
this.id = options.id;
|
||||
this.p_user_id = options.p_user_id;
|
||||
this.initAlias();
|
||||
this.needRebind = !!(this.p_user_id && !getCache('token'));
|
||||
this.getProductDetail();
|
||||
if (this.needRebind) {
|
||||
uni.$on('wx-login-ready', this.onWxLoginReady);
|
||||
// 登录可能已经在 onLoad 和 $on 之间完成,补一次检查
|
||||
if (getCache('token')) {
|
||||
this.onWxLoginReady();
|
||||
}
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('wx-login-ready', this.onWxLoginReady);
|
||||
},
|
||||
onShareAppMessage() {
|
||||
const userInfo = getCache('user_info')
|
||||
@@ -313,6 +350,10 @@ export default {
|
||||
uni.previewImage({ current: 0, urls: [`${url}?watermark/2/text/${w}/fontsize/800`] });
|
||||
},
|
||||
showModal() {
|
||||
if (!getCache('token')) {
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
this.getListOption();
|
||||
this.specPicker.visible = false;
|
||||
this.listModal = true;
|
||||
@@ -345,11 +386,31 @@ export default {
|
||||
this.initAlias();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 静默登录完成后补绑:首次详情请求没带上 token 时 inheritAgentPrice 不会写库
|
||||
*/
|
||||
onWxLoginReady() {
|
||||
if (!this.needRebind) return;
|
||||
this.needRebind = false;
|
||||
uni.$off('wx-login-ready', this.onWxLoginReady);
|
||||
this.getProductDetail();
|
||||
},
|
||||
initAlias() {
|
||||
if (this.product.alias) this.product.alias = this.product.alias.replace(/\n/g, '<br/>');
|
||||
},
|
||||
getListOption() {
|
||||
getCartListApi().then((res) => { this.listOption = res; });
|
||||
},
|
||||
toggleFav() {
|
||||
if (!this.id) return
|
||||
if (!getCache('token')) {
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
toggleFavoriteApi(this.id).then((res) => {
|
||||
this.product.favorited = !!(res && res.favorited)
|
||||
uni.showToast({ title: this.product.favorited ? '已收藏' : '已取消', icon: 'none' })
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -380,7 +441,33 @@ export default {
|
||||
.hero-overlay { position: absolute; left: 0; right: 0; bottom: 0; padding: 40rpx 32rpx 28rpx; background: linear-gradient(transparent, rgba(0, 0, 0, 0.45)); display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.hero-overlay-code { font-size: 40rpx; font-weight: 600; letter-spacing: 4rpx; }
|
||||
.hero-overlay-alias { font-size: 24rpx; opacity: 0.85; }
|
||||
.share-fixed-btn { position: fixed; top: 40rpx; right: 30rpx; width: 72rpx; height: 72rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.08); z-index: 999; }
|
||||
.nav-actions,
|
||||
.pc-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.pc-actions {
|
||||
justify-content: flex-end;
|
||||
padding: 12rpx 24rpx 0;
|
||||
}
|
||||
.nav-icon-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.nav-icon-btn::after {
|
||||
border: none;
|
||||
}
|
||||
.product-video { width: 100%; height: 420rpx; }
|
||||
.info-card { margin-top: 0; position: relative; z-index: 2; padding: 40rpx 30rpx; min-height: 500rpx; }
|
||||
.info--editorial .product-code { letter-spacing: 6rpx; font-weight: 700; }
|
||||
.info--split .title-section { display: flex; gap: 24rpx; align-items: flex-start; }
|
||||
|
||||
@@ -67,12 +67,16 @@
|
||||
:class="['list--' + catalogList, { 'is-grid': catalogList === 'grid' || catalogList === 'compact' || catalogList === 'airy' }]"
|
||||
v-else
|
||||
>
|
||||
<view class="waterfall-column">
|
||||
<view
|
||||
v-for="(col, colIndex) in productColumns"
|
||||
:key="'col-' + colIndex"
|
||||
class="waterfall-column"
|
||||
>
|
||||
<view
|
||||
v-for="(product, index) in leftProducts"
|
||||
v-for="(product, index) in col"
|
||||
:key="product.id"
|
||||
class="product-item animate-card-enter"
|
||||
:style="{ animationDelay: index * 0.1 + 's' }"
|
||||
:style="{ animationDelay: (index * 0.1 + colIndex * 0.05) + 's' }"
|
||||
@tap="navigateToDetail(product.id)"
|
||||
>
|
||||
<ThemeCanvasCard
|
||||
@@ -88,38 +92,6 @@
|
||||
:src="product.cover"
|
||||
mode="widthFix"
|
||||
class="product-image"
|
||||
@load="imageLoaded(index * 2)"
|
||||
/>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
<text class="product-code">{{ product.title }}</text>
|
||||
</view>
|
||||
</ThemeCanvasCard>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="waterfall-column">
|
||||
<view
|
||||
v-for="(product, index) in rightProducts"
|
||||
:key="product.id"
|
||||
class="product-item animate-card-enter"
|
||||
:style="{ animationDelay: (index * 0.1 + 0.05) + 's' }"
|
||||
@tap="navigateToDetail(product.id)"
|
||||
>
|
||||
<ThemeCanvasCard
|
||||
:cover="product.cover"
|
||||
:name="product.title"
|
||||
:show-price="false"
|
||||
:scheme="pageCardScheme"
|
||||
:caption="pageCardCaption"
|
||||
page="catalog"
|
||||
>
|
||||
<view class="img-box">
|
||||
<image
|
||||
:src="product.cover"
|
||||
mode="widthFix"
|
||||
class="product-image"
|
||||
@load="imageLoaded(index * 2 + 1)"
|
||||
/>
|
||||
</view>
|
||||
<view class="info-box">
|
||||
@@ -251,8 +223,6 @@ export default {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
imageLoaded(index) {
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,12 +41,16 @@
|
||||
:class="['list--' + searchList, { 'is-grid': searchList === 'grid' || searchList === 'compact' || searchList === 'airy' }]"
|
||||
v-else
|
||||
>
|
||||
<view class="waterfall-column">
|
||||
<view
|
||||
v-for="(col, colIndex) in productColumns"
|
||||
:key="'col-' + colIndex"
|
||||
class="waterfall-column"
|
||||
>
|
||||
<view
|
||||
v-for="(product, index) in leftProducts"
|
||||
v-for="(product, index) in col"
|
||||
:key="product.id"
|
||||
class="product-item animate-card-enter"
|
||||
:style="{ animationDelay: index * 0.05 + 's' }"
|
||||
:style="{ animationDelay: (index * 0.05 + colIndex * 0.03) + 's' }"
|
||||
@tap="navigateToDetail(product.id)"
|
||||
>
|
||||
<ThemeCanvasCard
|
||||
@@ -61,36 +65,6 @@
|
||||
:src="product.cover"
|
||||
mode="widthFix"
|
||||
class="product-image"
|
||||
@load="imageLoaded(index * 2)"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<text class="product-code">{{ product.title }}</text>
|
||||
</view>
|
||||
</ThemeCanvasCard>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="waterfall-column">
|
||||
<view
|
||||
v-for="(product, index) in rightProducts"
|
||||
:key="product.id"
|
||||
class="product-item animate-card-enter"
|
||||
:style="{ animationDelay: (index * 0.05 + 0.05) + 's' }"
|
||||
@tap="navigateToDetail(product.id)"
|
||||
>
|
||||
<ThemeCanvasCard
|
||||
:cover="product.cover"
|
||||
:name="product.title"
|
||||
:show-price="false"
|
||||
:scheme="pageCardScheme"
|
||||
:caption="pageCardCaption"
|
||||
page="search"
|
||||
>
|
||||
<image
|
||||
:src="product.cover"
|
||||
mode="widthFix"
|
||||
class="product-image"
|
||||
@load="imageLoaded(index * 2 + 1)"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<text class="product-code">{{ product.title }}</text>
|
||||
@@ -185,8 +159,6 @@ export default {
|
||||
uni.hideLoading()
|
||||
})
|
||||
},
|
||||
imageLoaded(index) {
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
63
pkg-more/address/form.vue
Normal file
63
pkg-more/address/form.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar :title="id ? '编辑地址' : '新增地址'" />
|
||||
<view class="form" :style="{ background: themeSurface }">
|
||||
<input v-model="form.name" class="input" placeholder="收件人" />
|
||||
<input v-model="form.phone" class="input" type="number" placeholder="手机号" />
|
||||
<input v-model="form.address" class="input" placeholder="详细地址" />
|
||||
<view class="switch-row" @tap="form.is_default = form.is_default === 1 ? 0 : 1">
|
||||
<text :style="{ color: themeText }">设为默认</text>
|
||||
<text :style="{ color: themePrimary }">{{ form.is_default === 1 ? '是' : '否' }}</text>
|
||||
</view>
|
||||
<button class="submit" :style="{ background: themePrimary, color: themeSurface }" @tap="save">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createAddressApi, getAddressDetailApi, updateAddressApi } from '@/api/page/address'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { id: 0, form: { name: '', phone: '', address: '', is_default: 0 } }
|
||||
},
|
||||
async onLoad(options) {
|
||||
await this.applyThemeOnShow()
|
||||
this.id = Number(options.id || 0)
|
||||
if (this.id) {
|
||||
getAddressDetailApi(this.id).then((res) => {
|
||||
this.form = {
|
||||
name: res.name || '',
|
||||
phone: res.phone || '',
|
||||
address: res.address || '',
|
||||
is_default: Number(res.is_default) === 1 ? 1 : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const payload = { ...this.form }
|
||||
const req = this.id
|
||||
? updateAddressApi({ id: this.id, ...payload })
|
||||
: createAddressApi(payload)
|
||||
req.then(() => {
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.form { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.input { height: 88rpx; border-bottom: 1rpx solid #eee; font-size: 28rpx; }
|
||||
.switch-row { display: flex; justify-content: space-between; padding: 24rpx 0; }
|
||||
.submit { margin-top: 24rpx; }
|
||||
</style>
|
||||
90
pkg-more/address/list.vue
Normal file
90
pkg-more/address/list.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="收货地址" />
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
:style="{ background: themeSurface }"
|
||||
@tap="pick(item)"
|
||||
>
|
||||
<view class="name" :style="{ color: themeText }">
|
||||
{{ item.name }} {{ item.phone }}
|
||||
<text v-if="item.is_default === 1" class="tag" :style="{ color: themePrimary }">默认</text>
|
||||
</view>
|
||||
<view class="addr" :style="{ color: themeTextSoft }">{{ item.address }}</view>
|
||||
<view class="ops">
|
||||
<text @tap.stop="edit(item)">编辑</text>
|
||||
<text @tap.stop="setDefault(item)">设为默认</text>
|
||||
<text @tap.stop="remove(item)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" v-if="!list.length" :style="{ color: themeTextSoft }">还没有地址</view>
|
||||
<button class="add" :style="{ background: themePrimary, color: themeSurface }" @tap="edit({})">新增地址</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deleteAddressApi, getAddressListApi, setDefaultAddressApi } from '@/api/page/address'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { list: [], pickMode: false }
|
||||
},
|
||||
onLoad(options) {
|
||||
this.pickMode = options.pick === '1'
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getAddressListApi().then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
pick(item) {
|
||||
if (!this.pickMode) return
|
||||
const pages = getCurrentPages()
|
||||
const prev = pages[pages.length - 2]
|
||||
if (prev && prev.$vm && prev.$vm.onPickAddress) {
|
||||
prev.$vm.onPickAddress(item)
|
||||
}
|
||||
uni.navigateBack()
|
||||
},
|
||||
edit(item) {
|
||||
const id = item.id ? `?id=${item.id}` : ''
|
||||
uni.navigateTo({ url: `/pkg-more/address/form${id}` })
|
||||
},
|
||||
setDefault(item) {
|
||||
setDefaultAddressApi(item.id).then(() => this.load())
|
||||
},
|
||||
remove(item) {
|
||||
uni.showModal({
|
||||
title: '删除地址',
|
||||
content: '确定删除该地址吗?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
deleteAddressApi(item.id).then(() => this.load())
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; padding-bottom: 160rpx; }
|
||||
.card { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.name { font-size: 30rpx; }
|
||||
.tag { margin-left: 12rpx; font-size: 22rpx; }
|
||||
.addr { font-size: 26rpx; margin-top: 8rpx; }
|
||||
.ops { display: flex; gap: 24rpx; margin-top: 16rpx; font-size: 24rpx; color: #999; }
|
||||
.empty { text-align: center; padding: 80rpx 0; }
|
||||
.add { position: fixed; left: 32rpx; right: 32rpx; bottom: 40rpx; }
|
||||
</style>
|
||||
79
pkg-more/after-sale/apply.vue
Normal file
79
pkg-more/after-sale/apply.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="申请售后" />
|
||||
<view class="form" :style="{ background: themeSurface }">
|
||||
<textarea v-model="reason" class="input" maxlength="255" placeholder="请说明售后原因" />
|
||||
<input v-model="amountYuan" class="amount" type="digit" placeholder="退款金额(元,不填按已付)" />
|
||||
<view class="images">
|
||||
<image v-for="(src, i) in images" :key="i" :src="src" class="thumb" @tap="removeImage(i)" />
|
||||
<view v-if="images.length < 3" class="add" :style="{ borderColor: themeBorder }" @tap="chooseImage">+</view>
|
||||
</view>
|
||||
<button class="submit" :style="{ background: themePrimary, color: themeSurface }" @tap="submit">提交申请</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createAfterSaleApi } from '@/api/page/afterSale'
|
||||
import { uploadVoucherImageApi } from '@/api/page/order'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { orderId: 0, reason: '', amountYuan: '', images: [] }
|
||||
},
|
||||
async onLoad(options) {
|
||||
await this.applyThemeOnShow()
|
||||
this.orderId = Number(options.order_id || 0)
|
||||
},
|
||||
methods: {
|
||||
chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 3 - this.images.length,
|
||||
success: async (res) => {
|
||||
for (const path of res.tempFilePaths || []) {
|
||||
const url = await uploadVoucherImageApi(path)
|
||||
this.images.push(url)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
removeImage(index) {
|
||||
this.images.splice(index, 1)
|
||||
},
|
||||
submit() {
|
||||
if (!this.reason.trim()) {
|
||||
uni.showToast({ title: '请填写原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const yuan = Number(this.amountYuan)
|
||||
const payload = {
|
||||
order_id: this.orderId,
|
||||
reason: this.reason,
|
||||
images: this.images,
|
||||
}
|
||||
if (!Number.isNaN(yuan) && yuan > 0) {
|
||||
payload.amount = Math.round(yuan * 100)
|
||||
}
|
||||
createAfterSaleApi(payload).then(() => {
|
||||
uni.showToast({ title: '已提交', icon: 'success' })
|
||||
setTimeout(() => uni.redirectTo({ url: '/pkg-more/after-sale/index' }), 400)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.form { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.input { width: 100%; min-height: 180rpx; font-size: 28rpx; }
|
||||
.amount { height: 80rpx; font-size: 28rpx; margin-top: 16rpx; }
|
||||
.images { display: flex; gap: 16rpx; margin: 16rpx 0; }
|
||||
.thumb, .add { width: 140rpx; height: 140rpx; border-radius: 8rpx; }
|
||||
.add { border: 2rpx dashed; display: flex; align-items: center; justify-content: center; font-size: 48rpx; }
|
||||
.submit { margin-top: 12rpx; }
|
||||
</style>
|
||||
63
pkg-more/after-sale/index.vue
Normal file
63
pkg-more/after-sale/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="我的售后" />
|
||||
<view v-if="!list.length" class="empty" :style="{ color: themeTextSoft }">还没有售后申请</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
:style="{ background: themeSurface }"
|
||||
>
|
||||
<view class="row">
|
||||
<text class="no" :style="{ color: themeText }">{{ item.order_no || '订单' }}</text>
|
||||
<text class="status" :style="{ color: statusColor(item.status) }">{{ item.status_text }}</text>
|
||||
</view>
|
||||
<text class="reason" :style="{ color: themeText }">{{ item.reason }}</text>
|
||||
<text class="amount" :style="{ color: themePrimary }">申请退款 ¥{{ item.amount_text || '0' }}</text>
|
||||
<text v-if="item.admin_remark" class="remark" :style="{ color: themeTextSoft }">
|
||||
商家:{{ item.admin_remark }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAfterSaleListApi } from '@/api/page/afterSale'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { list: [] }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getAfterSaleListApi().then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
statusColor(status) {
|
||||
if (status === 1) return this.themePrimary
|
||||
if (status === 2) return '#C2410C'
|
||||
return this.themeTextSoft
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; padding-bottom: 40rpx; }
|
||||
.empty { text-align: center; padding: 120rpx 0; }
|
||||
.card { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.no { font-size: 26rpx; }
|
||||
.status { font-size: 24rpx; }
|
||||
.reason { display: block; margin-top: 12rpx; font-size: 28rpx; }
|
||||
.amount, .remark { display: block; margin-top: 8rpx; font-size: 24rpx; }
|
||||
</style>
|
||||
45
pkg-more/colorcard/detail.vue
Normal file
45
pkg-more/colorcard/detail.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="色卡详情" />
|
||||
<image v-if="info.cover" :src="info.cover" class="hero" mode="aspectFill" @tap="preview" />
|
||||
<view class="card" :style="{ background: themeSurface }">
|
||||
<text class="name" :style="{ color: themeText }">{{ info.description || '色卡' }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">分类 {{ info.card_class_name || '-' }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">公司 {{ info.company_name || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getColorcardDetailApi } from '@/api/page/colorcard'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { info: {} }
|
||||
},
|
||||
async onLoad(options) {
|
||||
await this.applyThemeOnShow()
|
||||
getColorcardDetailApi(Number(options.id || 0)).then((res) => {
|
||||
this.info = res || {}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
preview() {
|
||||
if (!this.info.cover) return
|
||||
uni.previewImage({ current: this.info.cover, urls: [this.info.cover] })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.hero { width: 100%; height: 720rpx; }
|
||||
.card { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.name { font-size: 34rpx; font-weight: 600; display: block; }
|
||||
.meta { display: block; margin-top: 8rpx; font-size: 26rpx; }
|
||||
</style>
|
||||
74
pkg-more/colorcard/index.vue
Normal file
74
pkg-more/colorcard/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="色卡目录" />
|
||||
<scroll-view scroll-x class="tabs">
|
||||
<text
|
||||
v-for="item in classes"
|
||||
:key="item.id"
|
||||
class="tab"
|
||||
:style="{ color: classId === item.id ? themePrimary : themeTextSoft }"
|
||||
@tap="switchClass(item.id)"
|
||||
>{{ item.name }}</text>
|
||||
</scroll-view>
|
||||
<view v-if="!list.length" class="empty" :style="{ color: themeTextSoft }">暂无色卡</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
:style="{ background: themeSurface }"
|
||||
@tap="goDetail(item.id)"
|
||||
>
|
||||
<image :src="item.cover" class="cover" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<text class="title" :style="{ color: themeText }">{{ item.description || item.name || '色卡' }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ item.company_name }} {{ item.card_class_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getColorcardClassListApi, getColorcardListApi } from '@/api/page/colorcard'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { classes: [{ id: 0, name: '全部' }], classId: 0, list: [] }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
getColorcardClassListApi().then((res) => {
|
||||
this.classes = [{ id: 0, name: '全部' }, ...(res || [])]
|
||||
})
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getColorcardListApi({ card_class: this.classId || undefined }).then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
switchClass(id) {
|
||||
this.classId = id
|
||||
this.load()
|
||||
},
|
||||
goDetail(id) {
|
||||
uni.navigateTo({ url: `/pkg-more/colorcard/detail?id=${id}` })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.tabs { white-space: nowrap; padding: 16rpx 24rpx; }
|
||||
.tab { display: inline-block; margin-right: 28rpx; font-size: 28rpx; }
|
||||
.empty { text-align: center; padding: 120rpx 0; }
|
||||
.card { display: flex; margin: 24rpx; padding: 20rpx; border-radius: 16rpx; }
|
||||
.cover { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin-right: 20rpx; }
|
||||
.title { font-size: 30rpx; }
|
||||
.meta { display: block; margin-top: 8rpx; font-size: 24rpx; }
|
||||
</style>
|
||||
45
pkg-more/enterprise/detail.vue
Normal file
45
pkg-more/enterprise/detail.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="企业详情" />
|
||||
<view v-if="info.id" class="card" :style="{ background: themeSurface }">
|
||||
<image v-if="info.logo" :src="info.logo" class="logo" mode="aspectFill" />
|
||||
<text class="name" :style="{ color: themeText }">{{ info.name }}</text>
|
||||
<view class="row" :style="{ color: themeTextSoft }">联系人 {{ info.contact_name || '-' }}</view>
|
||||
<view class="row" :style="{ color: themeTextSoft }">电话 {{ info.phone || '-' }}</view>
|
||||
<view class="row" :style="{ color: themeTextSoft }">地址 {{ info.address || '-' }}</view>
|
||||
<view class="row" :style="{ color: themeTextSoft }">税号 {{ info.tax_no || '-' }}</view>
|
||||
</view>
|
||||
<view v-else class="empty" :style="{ color: themeTextSoft }">未绑定企业</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getEnterpriseApi } from '@/api/page/user'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { info: {} }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
getEnterpriseApi().then((res) => {
|
||||
this.info = res || {}
|
||||
}).catch(() => {
|
||||
this.info = {}
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.card { margin: 24rpx; padding: 32rpx; border-radius: 16rpx; }
|
||||
.logo { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-bottom: 16rpx; }
|
||||
.name { font-size: 36rpx; font-weight: 600; display: block; margin-bottom: 16rpx; }
|
||||
.row { font-size: 26rpx; line-height: 1.8; }
|
||||
.empty { padding: 120rpx 0; text-align: center; }
|
||||
</style>
|
||||
58
pkg-more/factory/detail.vue
Normal file
58
pkg-more/factory/detail.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="工厂详情" />
|
||||
<image v-if="info.cover" :src="info.cover" class="hero" mode="aspectFill" />
|
||||
<view class="card" :style="{ background: themeSurface }">
|
||||
<text class="name" :style="{ color: themeText }">{{ info.name }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ info.classification_name }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ info.phone }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ info.address }}</text>
|
||||
</view>
|
||||
<view class="grid">
|
||||
<image
|
||||
v-for="(img, i) in info.images || []"
|
||||
:key="i"
|
||||
:src="img.url || img.cover"
|
||||
class="grid-img"
|
||||
mode="aspectFill"
|
||||
@tap="preview(img.url || img.cover)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFactoryDetailApi } from '@/api/page/factory'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { info: {} }
|
||||
},
|
||||
async onLoad(options) {
|
||||
await this.applyThemeOnShow()
|
||||
getFactoryDetailApi(Number(options.id || 0)).then((res) => {
|
||||
this.info = res || {}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
preview(url) {
|
||||
const urls = (this.info.images || []).map((item) => item.url || item.cover).filter(Boolean)
|
||||
uni.previewImage({ current: url, urls })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.hero { width: 100%; height: 420rpx; }
|
||||
.card { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.name { font-size: 34rpx; font-weight: 600; display: block; }
|
||||
.meta { display: block; margin-top: 8rpx; font-size: 26rpx; }
|
||||
.grid { display: flex; flex-wrap: wrap; padding: 0 24rpx 40rpx; gap: 12rpx; }
|
||||
.grid-img { width: 226rpx; height: 226rpx; border-radius: 8rpx; }
|
||||
</style>
|
||||
74
pkg-more/factory/index.vue
Normal file
74
pkg-more/factory/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="工厂目录" />
|
||||
<scroll-view scroll-x class="tabs">
|
||||
<text
|
||||
v-for="item in classes"
|
||||
:key="item.id"
|
||||
class="tab"
|
||||
:style="{ color: classId === item.id ? themePrimary : themeTextSoft }"
|
||||
@tap="switchClass(item.id)"
|
||||
>{{ item.name }}</text>
|
||||
</scroll-view>
|
||||
<view v-if="!list.length" class="empty" :style="{ color: themeTextSoft }">暂无工厂</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
:style="{ background: themeSurface }"
|
||||
@tap="goDetail(item.id)"
|
||||
>
|
||||
<image :src="item.cover" class="cover" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<text class="title" :style="{ color: themeText }">{{ item.name }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ item.classification_name }} {{ item.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFactoryClassListApi, getFactoryListApi } from '@/api/page/factory'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { classes: [{ id: 0, name: '全部' }], classId: 0, list: [] }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
getFactoryClassListApi().then((res) => {
|
||||
this.classes = [{ id: 0, name: '全部' }, ...(res || [])]
|
||||
})
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getFactoryListApi({ classification: this.classId || undefined }).then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
switchClass(id) {
|
||||
this.classId = id
|
||||
this.load()
|
||||
},
|
||||
goDetail(id) {
|
||||
uni.navigateTo({ url: `/pkg-more/factory/detail?id=${id}` })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.tabs { white-space: nowrap; padding: 16rpx 24rpx; }
|
||||
.tab { display: inline-block; margin-right: 28rpx; font-size: 28rpx; }
|
||||
.empty { text-align: center; padding: 120rpx 0; }
|
||||
.card { display: flex; margin: 24rpx; padding: 20rpx; border-radius: 16rpx; }
|
||||
.cover { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin-right: 20rpx; }
|
||||
.title { font-size: 30rpx; }
|
||||
.meta { display: block; margin-top: 8rpx; font-size: 24rpx; }
|
||||
</style>
|
||||
55
pkg-more/favorite/index.vue
Normal file
55
pkg-more/favorite/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="我的收藏" />
|
||||
<view v-if="!list.length" class="empty" :style="{ color: themeTextSoft }">还没有收藏</view>
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="card"
|
||||
:style="{ background: themeSurface }"
|
||||
@tap="goDetail(item.catalogue_id)"
|
||||
>
|
||||
<image :src="item.cover" class="cover" mode="aspectFill" />
|
||||
<view class="info">
|
||||
<text class="title" :style="{ color: themeText }">{{ item.title }}</text>
|
||||
<text class="price" :style="{ color: themePrimary }">{{ item.price || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFavoriteListApi } from '@/api/page/favorite'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { list: [] }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
getFavoriteListApi().then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goDetail(id) {
|
||||
if (!id) return
|
||||
uni.navigateTo({ url: `/pages/product/detail?id=${id}` })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; }
|
||||
.empty { text-align: center; padding: 120rpx 0; }
|
||||
.card { display: flex; margin: 24rpx; padding: 20rpx; border-radius: 16rpx; }
|
||||
.cover { width: 160rpx; height: 160rpx; border-radius: 12rpx; margin-right: 20rpx; }
|
||||
.info { flex: 1; }
|
||||
.title { font-size: 28rpx; }
|
||||
.price { display: block; margin-top: 12rpx; font-size: 26rpx; }
|
||||
</style>
|
||||
82
pkg-more/feedback/index.vue
Normal file
82
pkg-more/feedback/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="page" :style="themePageStyle">
|
||||
<ThemeNavbar title="用户反馈" />
|
||||
<view class="form" :style="{ background: themeSurface }">
|
||||
<textarea v-model="content" class="input" maxlength="500" placeholder="请填写反馈内容" />
|
||||
<view class="images">
|
||||
<image v-for="(src, i) in images" :key="i" :src="src" class="thumb" @tap="removeImage(i)" />
|
||||
<view v-if="images.length < 3" class="add" :style="{ borderColor: themeBorder }" @tap="chooseImage">+</view>
|
||||
</view>
|
||||
<button class="submit" :style="{ background: themePrimary, color: themeSurface }" @tap="submit">提交</button>
|
||||
</view>
|
||||
<view v-for="item in list" :key="item.id" class="card" :style="{ background: themeSurface }">
|
||||
<text class="content" :style="{ color: themeText }">{{ item.content }}</text>
|
||||
<text class="meta" :style="{ color: themeTextSoft }">{{ item.status === 1 ? '已回复' : '待处理' }}</text>
|
||||
<text v-if="item.reply" class="reply" :style="{ color: themePrimary }">回复:{{ item.reply }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createFeedbackApi, getFeedbackListApi } from '@/api/page/feedback'
|
||||
import { uploadVoucherImageApi } from '@/api/page/order'
|
||||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||||
import themeMixin from '@/mixins/theme'
|
||||
|
||||
export default {
|
||||
components: { ThemeNavbar },
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return { content: '', images: [], list: [] }
|
||||
},
|
||||
async onShow() {
|
||||
await this.applyThemeOnShow()
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
getFeedbackListApi().then((res) => {
|
||||
this.list = res || []
|
||||
})
|
||||
},
|
||||
chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 3 - this.images.length,
|
||||
success: async (res) => {
|
||||
for (const path of res.tempFilePaths || []) {
|
||||
const url = await uploadVoucherImageApi(path)
|
||||
this.images.push(url)
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
removeImage(index) {
|
||||
this.images.splice(index, 1)
|
||||
},
|
||||
submit() {
|
||||
if (!this.content.trim()) {
|
||||
uni.showToast({ title: '请填写内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
createFeedbackApi({ content: this.content, images: this.images }).then(() => {
|
||||
this.content = ''
|
||||
this.images = []
|
||||
uni.showToast({ title: '已提交', icon: 'success' })
|
||||
this.load()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page { min-height: 100vh; padding-bottom: 40rpx; }
|
||||
.form, .card { margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
|
||||
.input { width: 100%; min-height: 160rpx; font-size: 28rpx; }
|
||||
.images { display: flex; gap: 16rpx; margin: 16rpx 0; }
|
||||
.thumb, .add { width: 140rpx; height: 140rpx; border-radius: 8rpx; }
|
||||
.add { border: 2rpx dashed; display: flex; align-items: center; justify-content: center; font-size: 48rpx; }
|
||||
.submit { margin-top: 12rpx; }
|
||||
.content { display: block; font-size: 28rpx; }
|
||||
.meta, .reply { display: block; font-size: 24rpx; margin-top: 8rpx; }
|
||||
</style>
|
||||
83
utils/device.js
Normal file
83
utils/device.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 运行环境:区分手机微信与 PC 微信(Windows / Mac)。
|
||||
* 大屏适配只认官方 platform,开发者工具手机预览不要走 PC 布局。
|
||||
*/
|
||||
|
||||
export const PC_PAGE_MAX_WIDTH = 1200
|
||||
|
||||
const PC_PLATFORMS = { windows: true, mac: true }
|
||||
|
||||
export function isPcWeixin() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
const platform = String(sys.platform || '').toLowerCase()
|
||||
return !!PC_PLATFORMS[platform]
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getWindowWidth() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync() || {}
|
||||
return Number(sys.windowWidth) || 375
|
||||
} catch (e) {
|
||||
return 375
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽屏商品列数:卡片跟着窗口变窄、一行变多。
|
||||
* 手机仍双列;iPad / PC 中窗起加列,避免 rpx 把两列撑得过大。
|
||||
*/
|
||||
export function goodsColsByWidth(width) {
|
||||
const w = Number(width) || 375
|
||||
if (w >= 1200) return 5
|
||||
if (w >= 900) return 4
|
||||
if (w >= 600) return 3
|
||||
return 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页分类宫格列数:格子比商品卡更小,更早加列。
|
||||
* mosaic / featured 这种非对称布局不要用这个函数去覆盖。
|
||||
*/
|
||||
export function catColsByWidth(width) {
|
||||
const w = Number(width) || 375
|
||||
if (w >= 1100) return 6
|
||||
if (w >= 800) return 5
|
||||
if (w >= 480) return 4
|
||||
return 3
|
||||
}
|
||||
|
||||
const widthListeners = []
|
||||
let resizeBound = false
|
||||
|
||||
function emitWindowWidth(res) {
|
||||
const size = (res && res.size) || {}
|
||||
const width = Number(size.windowWidth) || getWindowWidth()
|
||||
widthListeners.forEach((fn) => {
|
||||
try {
|
||||
fn(width)
|
||||
} catch (e) {
|
||||
// 单个页面抛错不影响其它订阅
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅窗口宽度。PC 拉伸窗口时通知页面改列数。
|
||||
* 返回取消订阅函数,页面销毁时必须调用。
|
||||
*/
|
||||
export function watchWindowWidth(fn) {
|
||||
if (typeof fn !== 'function') return function noop() {}
|
||||
widthListeners.push(fn)
|
||||
if (!resizeBound && typeof uni.onWindowResize === 'function') {
|
||||
uni.onWindowResize(emitWindowWidth)
|
||||
resizeBound = true
|
||||
}
|
||||
return function unwatch() {
|
||||
const idx = widthListeners.indexOf(fn)
|
||||
if (idx >= 0) widthListeners.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
25
utils/subscribe.js
Normal file
25
utils/subscribe.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getThemeFeatures } from '@/utils/theme'
|
||||
|
||||
/**
|
||||
* 在用户点击手势里申请订阅授权。
|
||||
* 微信要求授权必须跟在 tap 后面,付款/发货各一张,一次最多两张。
|
||||
*/
|
||||
export function requestOrderSubscribe(scenes = ['pay', 'ship']) {
|
||||
const features = getThemeFeatures()
|
||||
const ids = []
|
||||
scenes.forEach((scene) => {
|
||||
const id = scene === 'ship' ? features.subscribe_ship_tpl : features.subscribe_pay_tpl
|
||||
if (id && ids.indexOf(id) < 0) {
|
||||
ids.push(id)
|
||||
}
|
||||
})
|
||||
if (!ids.length) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
uni.requestSubscribeMessage({
|
||||
tmplIds: ids,
|
||||
complete: () => resolve(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export const FALLBACK_LAYOUT = {
|
||||
product: { gallery: 'swiper', price: 'inline', action: 'fixed' },
|
||||
list: { style: 'card' },
|
||||
mine: { header: 'gradient', menu: 'list' },
|
||||
package: { list: 'card' },
|
||||
effect: {
|
||||
transition: 'fade',
|
||||
skeleton: 'shimmer',
|
||||
@@ -97,6 +98,8 @@ export function applyTheme(theme) {
|
||||
setCache(STORAGE_SCHEMES_KEY, theme.card_schemes || {})
|
||||
setCache(STORAGE_FEATURES_KEY, {
|
||||
package_enabled: !!(theme.features && theme.features.package_enabled),
|
||||
subscribe_pay_tpl: (theme.features && theme.features.subscribe_pay_tpl) || '',
|
||||
subscribe_ship_tpl: (theme.features && theme.features.subscribe_ship_tpl) || '',
|
||||
})
|
||||
applyVarsToDom(vars)
|
||||
}
|
||||
@@ -109,6 +112,7 @@ function mergeLayout(layout) {
|
||||
product: { ...FALLBACK_LAYOUT.product, ...(src.product || {}) },
|
||||
list: { ...FALLBACK_LAYOUT.list, ...(src.list || {}) },
|
||||
mine: { ...FALLBACK_LAYOUT.mine, ...(src.mine || {}) },
|
||||
package: { ...FALLBACK_LAYOUT.package, ...(src.package || {}) },
|
||||
effect: { ...FALLBACK_LAYOUT.effect, ...(src.effect || {}) },
|
||||
card_scheme: src.card_scheme || FALLBACK_LAYOUT.card_scheme,
|
||||
chrome: {
|
||||
@@ -163,6 +167,8 @@ export function getThemeFeatures() {
|
||||
const cached = getCache(STORAGE_FEATURES_KEY) || {}
|
||||
return {
|
||||
package_enabled: !!cached.package_enabled,
|
||||
subscribe_pay_tpl: cached.subscribe_pay_tpl || '',
|
||||
subscribe_ship_tpl: cached.subscribe_ship_tpl || '',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user