From 00b00e0c95a85b8782fe50b48ef26764eb68a95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B4=E7=B3=95=E5=B4=BD=E5=B4=BD?= Date: Thu, 20 Aug 2026 19:15:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=8B=A5=E5=B9=B2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.vue | 6 + api/page/address.js | 31 +++ api/page/afterSale.js | 11 + api/page/cart.js | 5 + api/page/colorcard.js | 19 ++ api/page/factory.js | 15 ++ api/page/favorite.js | 11 + api/page/feedback.js | 11 + api/page/user.js | 4 + components/theme/ThemeCanvasCard.vue | 26 +- components/theme/ThemeNavbar.vue | 95 +++++++- manifest.json | 2 +- mixins/productWaterfall.js | 22 +- mixins/theme.js | 75 +++++- pages.json | 19 ++ pages/cart/detail.vue | 66 ++++- pages/goods/index.vue | 30 ++- pages/login/index.vue | 6 +- pages/mine/mine.vue | 352 +++++++++++++++++---------- pages/order/detail.vue | 65 ++++- pages/order/list.vue | 276 ++++++++++++--------- pages/package/detail.vue | 21 ++ pages/package/index.vue | 54 ++-- pages/product/detail.vue | 99 +++++++- pages/product/product.vue | 44 +--- pages/search/search.vue | 42 +--- pkg-more/address/form.vue | 63 +++++ pkg-more/address/list.vue | 90 +++++++ pkg-more/after-sale/apply.vue | 79 ++++++ pkg-more/after-sale/index.vue | 63 +++++ pkg-more/colorcard/detail.vue | 45 ++++ pkg-more/colorcard/index.vue | 74 ++++++ pkg-more/enterprise/detail.vue | 45 ++++ pkg-more/factory/detail.vue | 58 +++++ pkg-more/factory/index.vue | 74 ++++++ pkg-more/favorite/index.vue | 55 +++++ pkg-more/feedback/index.vue | 82 +++++++ utils/device.js | 83 +++++++ utils/subscribe.js | 25 ++ utils/theme.js | 6 + 40 files changed, 1862 insertions(+), 387 deletions(-) create mode 100644 api/page/address.js create mode 100644 api/page/afterSale.js create mode 100644 api/page/colorcard.js create mode 100644 api/page/factory.js create mode 100644 api/page/favorite.js create mode 100644 api/page/feedback.js create mode 100644 pkg-more/address/form.vue create mode 100644 pkg-more/address/list.vue create mode 100644 pkg-more/after-sale/apply.vue create mode 100644 pkg-more/after-sale/index.vue create mode 100644 pkg-more/colorcard/detail.vue create mode 100644 pkg-more/colorcard/index.vue create mode 100644 pkg-more/enterprise/detail.vue create mode 100644 pkg-more/factory/detail.vue create mode 100644 pkg-more/factory/index.vue create mode 100644 pkg-more/favorite/index.vue create mode 100644 pkg-more/feedback/index.vue create mode 100644 utils/device.js create mode 100644 utils/subscribe.js 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 @@