diff --git a/App.vue b/App.vue
index d073032..e5e3dcd 100644
--- a/App.vue
+++ b/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 {
\ No newline at end of file
diff --git a/api/page/address.js b/api/page/address.js
new file mode 100644
index 0000000..150ed11
--- /dev/null
+++ b/api/page/address.js
@@ -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 })
+}
diff --git a/api/page/afterSale.js b/api/page/afterSale.js
new file mode 100644
index 0000000..e8ef8cd
--- /dev/null
+++ b/api/page/afterSale.js
@@ -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)
+}
diff --git a/api/page/cart.js b/api/page/cart.js
index 97c5076..84befbb 100644
--- a/api/page/cart.js
+++ b/api/page/cart.js
@@ -48,3 +48,8 @@ export const updateItemApi = async (id, params = {}) => {
...params
})
}
+
+/** 导出清单报价文本/CSV */
+export const exportQuoteApi = async (id) => {
+ return await get(`${prefix}export-quote`, { id })
+}
diff --git a/api/page/colorcard.js b/api/page/colorcard.js
new file mode 100644
index 0000000..66c4cbc
--- /dev/null
+++ b/api/page/colorcard.js
@@ -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`)
+}
diff --git a/api/page/factory.js b/api/page/factory.js
new file mode 100644
index 0000000..8c70637
--- /dev/null
+++ b/api/page/factory.js
@@ -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`)
+}
diff --git a/api/page/favorite.js b/api/page/favorite.js
new file mode 100644
index 0000000..8bbbd54
--- /dev/null
+++ b/api/page/favorite.js
@@ -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 })
+}
diff --git a/api/page/feedback.js b/api/page/feedback.js
new file mode 100644
index 0000000..315f804
--- /dev/null
+++ b/api/page/feedback.js
@@ -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)
+}
diff --git a/api/page/user.js b/api/page/user.js
index 9916daa..0fa0cb4 100644
--- a/api/page/user.js
+++ b/api/page/user.js
@@ -15,3 +15,7 @@ export const updateNickNameApi = async (nick_name) => {
nick_name
})
}
+
+export const getEnterpriseApi = async () => {
+ return await get(`${prefix}enterprise`)
+}
diff --git a/components/theme/ThemeCanvasCard.vue b/components/theme/ThemeCanvasCard.vue
index c61d9e5..79b15af 100644
--- a/components/theme/ThemeCanvasCard.vue
+++ b/components/theme/ThemeCanvasCard.vue
@@ -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 {
diff --git a/manifest.json b/manifest.json
index e297b5d..1136e1f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -59,7 +59,7 @@
"urlCheck" : false,
"minified" : true
},
- // "resizable" : true,
+ "resizable" : true,
"usingComponents" : true
},
"mp-alipay" : {
diff --git a/mixins/productWaterfall.js b/mixins/productWaterfall.js
index 68aa0f8..50a5bc8 100644
--- a/mixins/productWaterfall.js
+++ b/mixins/productWaterfall.js
@@ -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() {
diff --git a/mixins/theme.js b/mixins/theme.js
index b078eb6..da91104 100644
--- a/mixins/theme.js
+++ b/mixins/theme.js
@@ -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()
},
diff --git a/pages.json b/pages.json
index 021e3bc..f993132 100644
--- a/pages.json
+++ b/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",
diff --git a/pages/cart/detail.vue b/pages/cart/detail.vue
index b33f783..e0addc0 100644
--- a/pages/cart/detail.vue
+++ b/pages/cart/detail.vue
@@ -152,6 +152,8 @@
+
+
@@ -172,7 +174,10 @@
-
+
+
+ 地址簿
+
@@ -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); }
diff --git a/pages/order/detail.vue b/pages/order/detail.vue
index e46732d..0602061 100644
--- a/pages/order/detail.vue
+++ b/pages/order/detail.vue
@@ -1,6 +1,6 @@
-
+
加载中...
订单不存在
@@ -127,6 +127,13 @@
>
确认收货
+
+ 申请售后
+
@@ -136,10 +143,13 @@
:overlay="true"
:close-on-overlay="false"
position="right"
- title="上传转账凭证"
@clickoverlay="closeVoucher"
>
-
+
+
+ 上传转账凭证
+ 关闭
+
请上传银行/微信转账截图,后台审核通过后订单状态自动推进。
@@ -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 {
diff --git a/pages/order/list.vue b/pages/order/list.vue
index cc09f5b..6e9fe71 100644
--- a/pages/order/list.vue
+++ b/pages/order/list.vue
@@ -1,74 +1,96 @@
-
+
{{ tab.label }}
-
-
- 加载中...
-
- 📭
- 暂无订单
-
+
+
+ 加载中...
+
+ 📭
+ 暂无订单
+
-
-
+
+
-
-
-
-
- +{{ order.items.length - 4 }}
+
+
+
+
+ +{{ order.items.length - 4 }}
+
+
+ 共 {{ order.item_count || 0 }} 件商品
+
+
+
- 共 {{ order.item_count || 0 }} 件商品
-
-
-
-
- 加载中...
- 没有更多了
-
+ 加载中...
+ 没有更多了
+
+
+
@@ -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;
}
diff --git a/pages/package/detail.vue b/pages/package/detail.vue
index 186cedf..deb59d2 100644
--- a/pages/package/detail.vue
+++ b/pages/package/detail.vue
@@ -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)
diff --git a/pages/package/index.vue b/pages/package/index.vue
index 94bad3d..bd28e0b 100644
--- a/pages/package/index.vue
+++ b/pages/package/index.vue
@@ -9,7 +9,7 @@
@@ -34,26 +34,29 @@
-
+
-
-
-
-
- SET {{ padNo(index + 1) }}
- {{ item.name }}
- {{ item.subtitle }}
- {{ item.item_count || 0 }} 件搭配
-
- ¥{{ item.original_amount_text }}
- ¥{{ item.package_amount_text }}
+
+
+
+
+
+ SET {{ padNo(index + 1) }}
+ {{ item.name }}
+ {{ item.subtitle }}
+ {{ item.item_count || 0 }} 件搭配
+
+ ¥{{ item.original_amount_text }}
+ ¥{{ item.package_amount_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;
diff --git a/pages/product/detail.vue b/pages/product/detail.vue
index bd39aed..6dd7af6 100644
--- a/pages/product/detail.vue
+++ b/pages/product/detail.vue
@@ -1,6 +1,25 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -240,6 +263,7 @@
diff --git a/pages/search/search.vue b/pages/search/search.vue
index e28a6ac..d682666 100644
--- a/pages/search/search.vue
+++ b/pages/search/search.vue
@@ -41,12 +41,16 @@
:class="['list--' + searchList, { 'is-grid': searchList === 'grid' || searchList === 'compact' || searchList === 'airy' }]"
v-else
>
-
+
-
- {{ product.title }}
-
-
-
-
-
-
-
-
-
{{ product.title }}
@@ -185,8 +159,6 @@ export default {
uni.hideLoading()
})
},
- imageLoaded(index) {
- },
},
}
diff --git a/pkg-more/address/form.vue b/pkg-more/address/form.vue
new file mode 100644
index 0000000..100b6b1
--- /dev/null
+++ b/pkg-more/address/form.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+ 设为默认
+ {{ form.is_default === 1 ? '是' : '否' }}
+
+
+
+
+
+
+
+
+
diff --git a/pkg-more/address/list.vue b/pkg-more/address/list.vue
new file mode 100644
index 0000000..1603844
--- /dev/null
+++ b/pkg-more/address/list.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+ {{ item.name }} {{ item.phone }}
+ 默认
+
+ {{ item.address }}
+
+ 编辑
+ 设为默认
+ 删除
+
+
+ 还没有地址
+
+
+
+
+
+
+
diff --git a/pkg-more/after-sale/apply.vue b/pkg-more/after-sale/apply.vue
new file mode 100644
index 0000000..427a2cf
--- /dev/null
+++ b/pkg-more/after-sale/apply.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/pkg-more/after-sale/index.vue b/pkg-more/after-sale/index.vue
new file mode 100644
index 0000000..900b970
--- /dev/null
+++ b/pkg-more/after-sale/index.vue
@@ -0,0 +1,63 @@
+
+
+
+ 还没有售后申请
+
+
+ {{ item.order_no || '订单' }}
+ {{ item.status_text }}
+
+ {{ item.reason }}
+ 申请退款 ¥{{ item.amount_text || '0' }}
+
+
+
+
+
+
+
+
diff --git a/pkg-more/colorcard/detail.vue b/pkg-more/colorcard/detail.vue
new file mode 100644
index 0000000..5c1ac7e
--- /dev/null
+++ b/pkg-more/colorcard/detail.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {{ info.description || '色卡' }}
+ 分类 {{ info.card_class_name || '-' }}
+ 公司 {{ info.company_name || '-' }}
+
+
+
+
+
+
+
diff --git a/pkg-more/colorcard/index.vue b/pkg-more/colorcard/index.vue
new file mode 100644
index 0000000..f1641ea
--- /dev/null
+++ b/pkg-more/colorcard/index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+ {{ item.name }}
+
+ 暂无色卡
+
+
+
+ {{ item.description || item.name || '色卡' }}
+ {{ item.company_name }} {{ item.card_class_name }}
+
+
+
+
+
+
+
+
diff --git a/pkg-more/enterprise/detail.vue b/pkg-more/enterprise/detail.vue
new file mode 100644
index 0000000..7fc0f0a
--- /dev/null
+++ b/pkg-more/enterprise/detail.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {{ info.name }}
+ 联系人 {{ info.contact_name || '-' }}
+ 电话 {{ info.phone || '-' }}
+ 地址 {{ info.address || '-' }}
+ 税号 {{ info.tax_no || '-' }}
+
+ 未绑定企业
+
+
+
+
+
+
diff --git a/pkg-more/factory/detail.vue b/pkg-more/factory/detail.vue
new file mode 100644
index 0000000..3fc6353
--- /dev/null
+++ b/pkg-more/factory/detail.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+ {{ info.name }}
+ {{ info.classification_name }}
+ {{ info.phone }}
+ {{ info.address }}
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg-more/factory/index.vue b/pkg-more/factory/index.vue
new file mode 100644
index 0000000..b435d45
--- /dev/null
+++ b/pkg-more/factory/index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+ {{ item.name }}
+
+ 暂无工厂
+
+
+
+ {{ item.name }}
+ {{ item.classification_name }} {{ item.phone }}
+
+
+
+
+
+
+
+
diff --git a/pkg-more/favorite/index.vue b/pkg-more/favorite/index.vue
new file mode 100644
index 0000000..6ae3d8b
--- /dev/null
+++ b/pkg-more/favorite/index.vue
@@ -0,0 +1,55 @@
+
+
+
+ 还没有收藏
+
+
+
+ {{ item.title }}
+ {{ item.price || '' }}
+
+
+
+
+
+
+
+
diff --git a/pkg-more/feedback/index.vue b/pkg-more/feedback/index.vue
new file mode 100644
index 0000000..7ab7fa7
--- /dev/null
+++ b/pkg-more/feedback/index.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+ +
+
+
+
+
+ {{ item.content }}
+ {{ item.status === 1 ? '已回复' : '待处理' }}
+ 回复:{{ item.reply }}
+
+
+
+
+
+
+
diff --git a/utils/device.js b/utils/device.js
new file mode 100644
index 0000000..1449d73
--- /dev/null
+++ b/utils/device.js
@@ -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)
+ }
+}
diff --git a/utils/subscribe.js b/utils/subscribe.js
new file mode 100644
index 0000000..87b714b
--- /dev/null
+++ b/utils/subscribe.js
@@ -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(),
+ })
+ })
+}
diff --git a/utils/theme.js b/utils/theme.js
index 53b0709..6d96d4d 100644
--- a/utils/theme.js
+++ b/utils/theme.js
@@ -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 || '',
}
}