迁移版本

This commit is contained in:
李琦
2026-10-13 14:04:22 +08:00
parent 6da262b134
commit cfb128d2f0
11 changed files with 1244 additions and 63 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
维伦

View File

@@ -27,4 +27,9 @@ export const toCartApi = async (listId, productId) => {
product_id: productId
})
}
export const submitHighIntentApi = async (list_id, intent_remark = '') => {
return await post(`${prefix}submit-high-intent`, { list_id, intent_remark })
}
export const savePreferSkuApi = async (params) => {
return await post(`${prefix}save-prefer-sku`, params)
}

View File

@@ -1,5 +1,5 @@
import {get} from '../request';
import {get, post} from '../request';
const prefix = 'product/'
@@ -12,3 +12,6 @@ export const getCategoryListByPidApi = async (pid = 0) => {
export const getProductDetailApi = async (id = 0, p_user_id = 0) => {
return await get(`${prefix}detail`, {id, p_user_id})
}
export const productViewApi = async (id) => {
return await post(`${prefix}view`, { id })
}

View File

@@ -15,3 +15,6 @@ export const updateNickNameApi = async (nick_name) => {
nick_name
})
}
export const updateProfileApi = async (nick_name, avatar) => {
return await post(`${prefix}update-profile`, { nick_name, avatar })
}

View File

@@ -63,6 +63,12 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/cart/quote",
"style": {
"navigationBarTitleText": "查看报价"
}
},
{
"path": "pages/search/search",
"style": {

View File

@@ -29,6 +29,11 @@
<view class="top-left">
<u-icon name="calendar" color="#999" size="14"></u-icon>
<text class="date-text">{{ item.created_at }}</text>
<view class="list-status-badges">
<text v-if="item.is_high_intent" class="list-badge intent">已申请</text>
<text v-if="item.quote_status === 2" class="list-badge quoted">已报价</text>
<text v-else-if="item.quote_status === 1" class="list-badge quoting">报价中</text>
</view>
</view>
<!-- 显性删除按钮:更直观,防误触 -->
<view class="delete-btn" @tap.stop="handleDelete(index, item.id)">
@@ -319,6 +324,35 @@ view, scroll-view, text, image {
font-family: Arial, sans-serif;
}
.list-status-badges {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-left: 16rpx;
}
.list-badge {
font-size: 18rpx;
padding: 2rpx 10rpx;
border-radius: 4rpx;
font-weight: 600;
}
.list-badge.intent {
color: #B4854D;
background: rgba(180, 133, 77, 0.1);
}
.list-badge.quoted {
color: #fff;
background: #B4854D;
}
.list-badge.quoting {
color: #B4854D;
border: 1rpx solid rgba(180, 133, 77, 0.4);
}
.delete-btn {
padding: 10rpx;
margin-right: -10rpx; /* 增加点击热区修正视觉位置 */

View File

@@ -1,6 +1,5 @@
<template>
<view class="container">
<!-- 顶部 Header: 极简大气 -->
<view class="detail-header">
<view class="header-content">
<view class="title-group">
@@ -8,42 +7,40 @@
<text class="page-subtitle">高端臻选系列</text>
</view>
<view class="header-badges">
<view class="status-badge-row" v-if="is_high_intent || quote_status === 2">
<text v-if="is_high_intent" class="status-badge intent">已申请</text>
<text v-if="quote_status === 2" class="status-badge quoted">已报价</text>
</view>
<text class="count-badge">共计 {{ info.items ? info.items.length : 0 }} 件</text>
<text class="id-badge">清单号: {{ listId }}</text>
</view>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="list-container" scroll-y>
<view
v-for="(item, index) in info.items"
:key="index"
:key="item.id || index"
class="product-card animate-up"
:style="{ animationDelay: index * 0.05 + 's' }"
>
<!-- 1. 卡片顶部工具栏:此处替代了 u-swipe-action -->
<!-- 彻底移除了滑动组件,改为显性的删除按钮 -->
<view class="card-top-bar" @tap="navigateToDetail(item.product.id)">
<view class="top-left">
<text class="index-num">NO.{{ index + 1 < 10 ? '0' + (index + 1) : index + 1 }}</text>
<text class="divider-v">|</text>
<text class="status-text">已选配置</text>
<text class="status-text" :class="{ 'status-text--active': item.has_prefer_sku }">{{ specStatusText(item) }}</text>
</view>
<view class="top-right">
<!-- 删除按钮 -->
<view class="action-btn delete" @tap.stop="handleDelete(index, item.id)">
<u-icon name="trash" color="#999" size="18"></u-icon>
</view>
<!-- 详情跳转图标 -->
<view class="action-btn arrow">
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</view>
</view>
<!-- 2. 产品主体内容:左图右文,极简排版 -->
<view class="card-body" @tap="navigateToDetail(item.product.id)">
<view class="image-wrapper">
<image
@@ -57,10 +54,10 @@
<view class="p-tags">
<text class="luxury-tag">甄选家具</text>
</view>
<text v-if="item.selected_sku" class="selected-sku-hint">{{ item.selected_sku }}</text>
</view>
</view>
<!-- 3. 规格清单:轻奢菜单风格 -->
<view class="specs-panel">
<view class="specs-inner">
<view
@@ -73,11 +70,19 @@
</view>
<view class="spec-detail-row">
<!-- 移除 nowrap 防止长文本撑开 -->
<text class="dim-text" v-if="spec.dimension">{{ spec.dimension }}</text>
<view class="dashed-line"></view>
<view class="price-group">
<view v-for="(pItem, pIndex) in spec.routine" :key="pIndex" class="price-unit">
<view
v-for="(pItem, pIndex) in spec.routine"
:key="pIndex"
class="price-unit"
:class="{
'price-unit--selected': canSelectSku && isSpecSelected(item, spec, pIndex),
'price-unit--disabled': !canSelectSku
}"
@tap.stop="canSelectSku && handlePreferSku(item, spec, pIndex)"
>
<text class="currency">¥</text>{{ pItem }}
</view>
</view>
@@ -91,12 +96,90 @@
<view class="safe-bottom"></view>
</scroll-view>
<view class="action-bar-wrapper safe-area-bottom">
<view class="action-bar">
<button
v-if="quote_visible"
class="bar-btn primary"
@tap="goToQuote"
>查看报价</button>
<button
v-if="!is_high_intent"
class="bar-btn outline"
@tap="openApplyPopup"
>申请报价</button>
<view v-else-if="!quote_visible" class="intent-status-bar">
<text>已申请报价</text>
</view>
</view>
</view>
<!-- 申请报价弹层 -->
<u-popup :show="applyPopupShow" mode="bottom" round="16" @close="closeApplyPopup">
<view class="apply-popup">
<view class="apply-popup-title">申请报价</view>
<view class="apply-field">
<text class="apply-label">备注(选填)</text>
<textarea
class="apply-remark"
v-model="intentRemark"
maxlength="500"
placeholder="可填写特殊需求、交期等"
/>
</view>
<view v-if="quote_apply_require_sku" class="apply-sku-section">
<text class="apply-label">请确认规格</text>
<scroll-view scroll-y class="apply-sku-list">
<view
v-for="(item, index) in info.items"
:key="item.id || index"
class="apply-sku-row"
@tap="openSkuPicker(item)"
>
<image class="apply-sku-cover" :src="item.product.cover" mode="aspectFill" />
<view class="apply-sku-info">
<text class="apply-sku-title">{{ item.product.title }}</text>
<text
class="apply-sku-value"
:class="{ 'apply-sku-value--empty': !item.selected_sku }"
>{{ item.selected_sku || '未选' }}</text>
</view>
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</scroll-view>
</view>
<button class="apply-confirm-btn" @tap="confirmApply">确认申请</button>
</view>
</u-popup>
<!-- 快速选 SKU -->
<u-popup :show="skuPickerShow" mode="bottom" round="16" @close="closeSkuPicker">
<view class="sku-picker">
<view class="sku-picker-title">{{ skuPickerTitle }}</view>
<scroll-view scroll-y class="sku-picker-list">
<view
v-for="(opt, idx) in skuPickerOptions"
:key="idx"
class="sku-picker-item"
:class="{ 'sku-picker-item--active': isPickerOptionSelected(opt) }"
@tap="pickSkuOption(opt)"
>
<text class="sku-picker-label">{{ opt.displayLabel }}</text>
<text class="sku-picker-price">{{ opt.priceText }}</text>
</view>
<view v-if="!skuPickerOptions.length" class="sku-picker-empty">暂无可选规格</view>
</scroll-view>
</view>
</u-popup>
<u-toast ref="uToast"></u-toast>
</view>
</template>
<script>
import {deleteCartItemApi, getCartItemApi} from "@/api/page/cart";
import {deleteCartItemApi, getCartItemApi, submitHighIntentApi, savePreferSkuApi} from "@/api/page/cart";
export default {
data() {
@@ -104,20 +187,152 @@ export default {
listId: '',
listName: '清单详情',
info: { items: [] },
// swipeOptions 已彻底移除
is_high_intent: false,
quote_status: 0,
quote_visible: false,
quote_final_total: null,
quote_apply_require_sku: false,
applyPopupShow: false,
intentRemark: '',
skuPickerShow: false,
skuPickerItem: null,
skuPickerOptions: [],
}
},
computed: {
canSelectSku() {
return this.quote_apply_require_sku && !this.quote_visible;
},
skuPickerTitle() {
const t = this.skuPickerItem && this.skuPickerItem.product
? this.skuPickerItem.product.title
: '';
return t ? `选择规格 - ${t}` : '选择规格';
},
},
onLoad(options) {
this.listId = options.id
this.getInfo();
},
methods: {
specStatusText(item) {
if (this.quote_visible && item.has_prefer_sku) {
return '报价规格';
}
if (item.has_prefer_sku || item.selected_sku) {
return '已预选';
}
return this.canSelectSku ? '点击预选规格' : '规格价目';
},
isSpecSelected(item, spec, pIndex) {
const sheetId = Number(spec.id);
if (item.price_sheet_id === sheetId && item.routine_index === pIndex) {
return true;
}
return false;
},
handlePreferSku(item, spec, pIndex) {
if (!this.canSelectSku) {
return;
}
if (this.isSpecSelected(item, spec, pIndex)) {
return;
}
this.savePreferSku(item, spec.id, pIndex);
},
savePreferSku(item, priceSheetId, routineIndex) {
uni.showLoading({ title: '保存中...' });
return savePreferSkuApi({
list_id: Number(this.listId),
list_item_id: item.id,
price_sheet_id: priceSheetId,
routine_index: routineIndex,
}).then((res) => {
const data = res || {};
item.price_sheet_id = data.price_sheet_id ?? priceSheetId;
item.routine_index = data.routine_index ?? routineIndex;
item.selected_sku = data.selected_sku ?? item.selected_sku;
item.has_prefer_sku = true;
uni.hideLoading();
uni.showToast({ title: '已预选规格', icon: 'success' });
return data;
}).catch(() => {
uni.hideLoading();
});
},
parseRoutineLabel(raw) {
const s = String(raw || '').trim();
if (!s) return '常规';
const m = s.match(/^(.*?)[::](.+)$/);
if (m) {
return (m[1] || '').trim() || '常规';
}
return '常规';
},
buildSkuOptions(item) {
const sheets = (item && item.product && item.product.price_sheet) || [];
const options = [];
sheets.forEach((spec) => {
const parts = [spec.specification, spec.dimension].filter(Boolean);
const sheetTitle = parts.join(' / ') || '规格';
const routines = Array.isArray(spec.routine) ? spec.routine : [];
if (!routines.length) {
options.push({
price_sheet_id: spec.id,
routine_index: 0,
displayLabel: sheetTitle,
priceText: '',
sheetTitle,
label: '常规',
});
return;
}
routines.forEach((raw, pIndex) => {
const label = this.parseRoutineLabel(raw);
const displayLabel = label && label !== '常规'
? `${sheetTitle} / ${label}`
: sheetTitle;
options.push({
price_sheet_id: spec.id,
routine_index: pIndex,
displayLabel,
priceText: String(raw || ''),
sheetTitle,
label,
});
});
});
return options;
},
openSkuPicker(item) {
this.skuPickerItem = item;
this.skuPickerOptions = this.buildSkuOptions(item);
this.skuPickerShow = true;
},
closeSkuPicker() {
this.skuPickerShow = false;
this.skuPickerItem = null;
this.skuPickerOptions = [];
},
isPickerOptionSelected(opt) {
const item = this.skuPickerItem;
if (!item) return false;
return Number(item.price_sheet_id) === Number(opt.price_sheet_id)
&& Number(item.routine_index) === Number(opt.routine_index);
},
pickSkuOption(opt) {
const item = this.skuPickerItem;
if (!item) return;
this.savePreferSku(item, opt.price_sheet_id, opt.routine_index).then(() => {
this.closeSkuPicker();
});
},
handleDelete(index, id) {
uni.showModal({
title: '移除确认',
content: '将从您的甄选清单中移除此商品',
confirmText: '移除',
confirmColor: '#B4854D', // 金色确认按钮
confirmColor: '#B4854D',
cancelColor: '#999999',
success: (res) => {
if (res.confirm) {
@@ -133,12 +348,50 @@ export default {
})
},
getInfo() {
uni.showLoading({ title: '加载中...' }) // 中文Loading
uni.showLoading({ title: '加载中...' })
getCartItemApi(this.listId).then((res) => {
this.info = res || { items: [] };
const data = res || { items: [] };
this.info = data;
this.listName = data.name || '清单详情';
this.is_high_intent = !!data.is_high_intent;
this.quote_status = data.quote_status || 0;
this.quote_visible = !!data.quote_visible;
this.quote_final_total = data.quote_final_total;
this.quote_apply_require_sku = Number(data.quote_apply_require_sku || 0) === 1;
this.intentRemark = data.intent_remark || '';
uni.hideLoading()
})
},
goToQuote() {
uni.navigateTo({
url: `/pages/cart/quote?id=${this.listId}`,
});
},
openApplyPopup() {
this.applyPopupShow = true;
},
closeApplyPopup() {
this.applyPopupShow = false;
},
confirmApply() {
if (this.quote_apply_require_sku) {
const items = this.info.items || [];
const missing = items.some((it) => !it.selected_sku);
if (missing) {
uni.showToast({ title: '请先选择全部规格', icon: 'none' });
return;
}
}
uni.showLoading({ title: '提交中...' });
submitHighIntentApi(this.listId, this.intentRemark || '').then(() => {
uni.hideLoading();
this.closeApplyPopup();
uni.showToast({ title: '已申请报价', icon: 'success' });
this.getInfo();
}).catch(() => {
uni.hideLoading();
});
},
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/product/detail?id=${id}`,
@@ -149,19 +402,17 @@ export default {
</script>
<style lang="scss">
/* 核心修复:强制使用 border-box 盒模型,防止 padding 撑开宽度 */
view, scroll-view, text, image {
box-sizing: border-box;
}
/* 全局容器:高级灰背景 */
.container {
min-height: 100vh;
width: 100%; /* 确保容器宽度 */
width: 100%;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
overflow-x: hidden; /* 防止水平溢出 */
overflow-x: hidden;
}
@keyframes slideUp {
@@ -170,7 +421,6 @@ view, scroll-view, text, image {
}
.animate-up { animation: slideUp 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; }
/* 1. Header 区域:极简,大量留白 */
.detail-header {
width: 100%;
background: #fff;
@@ -192,16 +442,16 @@ view, scroll-view, text, image {
.page-title {
font-size: 48rpx;
font-weight: 300; /* 细字体显得更高级 */
font-weight: 300;
color: #1A1A1A;
letter-spacing: 2rpx;
margin-bottom: 8rpx;
font-family: serif; /* 尝试使用衬线风格(取决于系统支持) */
font-family: serif;
}
.page-subtitle {
font-size: 18rpx;
color: #B4854D; /* 金色副标题 */
color: #B4854D;
letter-spacing: 6rpx;
font-weight: 600;
text-transform: uppercase;
@@ -214,6 +464,30 @@ view, scroll-view, text, image {
gap: 6rpx;
}
.status-badge-row {
display: flex;
gap: 10rpx;
margin-bottom: 8rpx;
}
.status-badge {
font-size: 18rpx;
padding: 4rpx 12rpx;
border-radius: 4rpx;
font-weight: 600;
letter-spacing: 1rpx;
}
.status-badge.intent {
color: #B4854D;
background: rgba(180, 133, 77, 0.12);
}
.status-badge.quoted {
color: #fff;
background: #B4854D;
}
.count-badge, .id-badge {
font-size: 20rpx;
color: #999;
@@ -221,26 +495,23 @@ view, scroll-view, text, image {
letter-spacing: 1rpx;
}
/* 2. 列表容器 */
.list-container {
flex: 1;
width: 100%; /* 明确宽度 */
width: 100%;
padding: 30rpx;
}
/* 3. 商品卡片:核心设计 */
.product-card {
width: 100%; /* 确保卡片不超出父容器 */
width: 100%;
background: #fff;
margin-bottom: 30rpx;
border-radius: 12rpx; /* 不需要太大的圆角 */
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.04); /* 大而柔和的阴影 */
border-radius: 12rpx;
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.04);
overflow: hidden;
position: relative;
border: 1rpx solid rgba(0,0,0,0.02); /* 极细的边框 */
border: 1rpx solid rgba(0,0,0,0.02);
}
/* -- 卡片顶部工具栏 -- */
.card-top-bar {
display: flex;
justify-content: space-between;
@@ -253,6 +524,8 @@ view, scroll-view, text, image {
.top-left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.index-num {
@@ -275,10 +548,16 @@ view, scroll-view, text, image {
letter-spacing: 1rpx;
}
.status-text--active {
color: #B4854D;
font-weight: 600;
}
.top-right {
display: flex;
align-items: center;
gap: 30rpx; /* 按钮之间拉开距离 */
gap: 30rpx;
flex-shrink: 0;
}
.action-btn {
@@ -289,11 +568,10 @@ view, scroll-view, text, image {
height: 44rpx;
}
/* -- 卡片主体 (图片+标题) -- */
.card-body {
padding: 30rpx;
display: flex;
align-items: center; /* 垂直居中 */
align-items: center;
gap: 30rpx;
}
@@ -304,7 +582,7 @@ view, scroll-view, text, image {
overflow: hidden;
background: #F9F9F9;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
flex-shrink: 0; /* 防止图片被压缩 */
flex-shrink: 0;
}
.product-thumb {
@@ -317,7 +595,7 @@ view, scroll-view, text, image {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden; /* 防止文字过长溢出 */
overflow: hidden;
}
.p-title {
@@ -326,7 +604,6 @@ view, scroll-view, text, image {
font-weight: 500;
line-height: 1.5;
margin-bottom: 16rpx;
/* 增加截断,防止超长标题撑开 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -339,20 +616,26 @@ view, scroll-view, text, image {
.luxury-tag {
font-size: 18rpx;
color: #B4854D;
background: rgba(180, 133, 77, 0.08); /* 极淡的金色背景 */
background: rgba(180, 133, 77, 0.08);
padding: 4rpx 12rpx;
border-radius: 4rpx;
letter-spacing: 1rpx;
font-weight: 600;
}
/* -- 规格清单面板 (菜单式) -- */
.selected-sku-hint {
margin-top: 12rpx;
font-size: 22rpx;
color: #888;
line-height: 1.4;
}
.specs-panel {
padding: 0 30rpx 40rpx; /* 底部留白 */
padding: 0 30rpx 40rpx;
}
.specs-inner {
background: #FAFAFA; /* 浅灰底 */
background: #FAFAFA;
border: 1rpx solid #EFEFEF;
border-radius: 8rpx;
padding: 24rpx;
@@ -386,34 +669,47 @@ view, scroll-view, text, image {
.dim-text {
font-size: 22rpx;
color: #888;
/* 移除 nowrap,允许换行,防止长尺寸描述撑开容器 */
/* white-space: nowrap; */
padding-right: 10rpx;
}
/* 虚线连接符 */
.dashed-line {
flex: 1;
height: 1px;
border-bottom: 2rpx dashed #E0E0E0;
margin: 0 16rpx;
position: relative;
top: -6rpx; /* 微调对齐 */
min-width: 40rpx; /* 保证至少有一点虚线 */
top: -6rpx;
min-width: 40rpx;
}
.price-group {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0; /* 防止价格被挤压 */
flex-shrink: 0;
gap: 12rpx;
}
.price-unit {
font-family: 'Times New Roman', serif; /* 衬线体价格,显贵 */
font-size: 30rpx;
font-family: 'Times New Roman', serif;
font-size: 28rpx;
color: #B4854D;
font-weight: bold;
padding: 12rpx 20rpx;
border-radius: 8rpx;
border: 1rpx solid transparent;
background: #fff;
transition: all 0.15s ease;
}
.price-unit--selected {
border-color: #B4854D;
background: rgba(180, 133, 77, 0.12);
box-shadow: 0 4rpx 12rpx rgba(180, 133, 77, 0.15);
}
.price-unit--disabled {
opacity: 0.85;
}
.currency {
@@ -423,6 +719,223 @@ view, scroll-view, text, image {
}
.safe-bottom {
height: 60rpx;
height: 200rpx;
}
</style>
.action-bar-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 100;
}
.action-bar {
display: flex;
align-items: stretch;
gap: 20rpx;
width: 100%;
min-height: 88rpx;
}
.bar-btn {
flex: 1;
margin: 0;
font-size: 30rpx;
padding: 0 24rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 12rpx;
}
.bar-btn::after {
border: none;
}
.bar-btn.primary {
background: #B4854D;
color: #fff;
font-weight: 600;
}
.bar-btn.outline {
background: #fff;
color: #B4854D;
border: 1rpx solid #B4854D;
font-weight: 600;
}
.intent-status-bar {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #F3F1EC;
border-radius: 12rpx;
border: 1rpx solid #E8E4DC;
}
.intent-status-bar text {
font-size: 30rpx;
color: #999;
letter-spacing: 2rpx;
}
.apply-popup {
padding: 36rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
max-height: 80vh;
display: flex;
flex-direction: column;
}
.apply-popup-title {
font-size: 34rpx;
font-weight: 600;
color: #1A1A1A;
text-align: center;
margin-bottom: 28rpx;
}
.apply-field {
margin-bottom: 24rpx;
}
.apply-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
}
.apply-remark {
width: 100%;
min-height: 140rpx;
padding: 20rpx;
background: #F7F8FA;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
}
.apply-sku-section {
flex: 1;
min-height: 0;
margin-bottom: 24rpx;
}
.apply-sku-list {
max-height: 420rpx;
}
.apply-sku-row {
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx 0;
border-bottom: 1rpx solid #F0F0F0;
}
.apply-sku-cover {
width: 88rpx;
height: 88rpx;
border-radius: 8rpx;
background: #F5F5F5;
flex-shrink: 0;
}
.apply-sku-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.apply-sku-title {
font-size: 28rpx;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.apply-sku-value {
font-size: 24rpx;
color: #B4854D;
}
.apply-sku-value--empty {
color: #E07040;
}
.apply-confirm-btn {
margin: 0;
height: 88rpx;
line-height: 88rpx;
background: #B4854D;
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 12rpx;
}
.apply-confirm-btn::after {
border: none;
}
.sku-picker {
padding: 36rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
max-height: 70vh;
display: flex;
flex-direction: column;
}
.sku-picker-title {
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
text-align: center;
margin-bottom: 20rpx;
}
.sku-picker-list {
max-height: 560rpx;
}
.sku-picker-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20rpx;
padding: 28rpx 20rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.sku-picker-item--active {
background: rgba(180, 133, 77, 0.08);
}
.sku-picker-label {
flex: 1;
font-size: 28rpx;
color: #333;
line-height: 1.4;
}
.sku-picker-price {
font-size: 26rpx;
color: #B4854D;
flex-shrink: 0;
}
.sku-picker-empty {
padding: 60rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
</style>

463
pages/cart/quote.vue Normal file
View File

@@ -0,0 +1,463 @@
<template>
<view class="page">
<scroll-view class="page-scroll" scroll-y>
<view class="quote-sheet">
<view class="sheet-header">
<view class="brand-block">
<view class="brand-mark" />
<view class="brand-text">
<text class="brand-name">产品报价</text>
<text class="brand-sub">PRODUCT QUOTATION</text>
</view>
</view>
<view class="header-right">
<text class="sheet-title">报价单</text>
<text class="sheet-list-name">清单:{{ listName }}</text>
</view>
</view>
<view class="meta-grid">
<view class="meta-item">
<text class="meta-label">清单名</text>
<text class="meta-value">{{ listName }}</text>
</view>
<view class="meta-item">
<text class="meta-label">商品数</text>
<text class="meta-value">{{ quoteItems.length }} 件</text>
</view>
<view class="meta-item">
<text class="meta-label">明细合计</text>
<text class="meta-value">¥{{ formatMoney(quote_items_total) }}</text>
</view>
<view class="meta-item" v-if="showPackagePrefer">
<text class="meta-label">套餐优惠总价</text>
<text class="meta-value accent">¥{{ formatMoney(quote_preferential_total) }}</text>
</view>
</view>
<view class="items-section">
<view
v-for="(item, index) in quoteItems"
:key="item.id || index"
class="quote-row"
>
<text class="row-index">{{ index + 1 }}</text>
<image
class="row-thumb"
:src="itemCover(item)"
mode="aspectFill"
/>
<view class="row-body">
<text class="row-title">{{ itemTitle(item) }}</text>
<text v-if="itemIdentifier(item)" class="row-id">{{ itemIdentifier(item) }}</text>
<view v-if="item.selected_sku" class="sku-block">
<text class="sku-tag">预选规格</text>
<text class="sku-text">{{ item.selected_sku }}</text>
</view>
<view class="row-prices">
<view class="price-line" v-if="item.unit_price != null && item.unit_price !== ''">
<text class="pl-label">单价</text>
<text class="pl-val">¥{{ formatMoney(item.unit_price) }}</text>
</view>
<view class="price-line" v-if="item.preferential_price != null && item.preferential_price !== ''">
<text class="pl-label">优惠价</text>
<text class="pl-val accent">¥{{ formatMoney(item.preferential_price) }}</text>
</view>
<view class="price-line final">
<text class="pl-label">成交价</text>
<text class="pl-val final-val">¥{{ formatMoney(item.final_price) }}</text>
</view>
</view>
</view>
</view>
<view v-if="quoteItems.length === 0" class="empty-hint">
<text>暂无报价明细</text>
</view>
</view>
<view class="total-section">
<text class="total-left">已选规格 {{ preferSkuCount }} / {{ quoteItems.length }} 件计入合计</text>
<view class="total-right">
<text class="total-label">总计金额</text>
<text class="total-amount">¥{{ quoteFinalTotalDisplay }}</text>
</view>
</view>
<view class="sheet-footer">
<text class="footer-note">说明:本报价按清单当前已选规格生成;未选规格商品不计入合计。价格如有变动以最终确认为准。</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { getCartItemApi } from '@/api/page/cart';
export default {
data() {
return {
listId: '',
listName: '-',
quote_final_total: null,
quote_items_total: null,
quote_preferential_total: null,
info: { items: [] },
};
},
computed: {
quoteItems() {
const items = this.info.items || [];
return items.filter((item) => item.has_quote);
},
preferSkuCount() {
return this.quoteItems.filter((item) => item.selected_sku).length;
},
quoteFinalTotalDisplay() {
if (this.quote_final_total == null || this.quote_final_total === '') {
return '--';
}
return this.formatMoney(this.quote_final_total);
},
showPackagePrefer() {
return (
this.quote_preferential_total != null &&
this.quote_preferential_total !== '' &&
Number(this.quote_preferential_total) !== Number(this.quote_items_total)
);
},
},
onLoad(options) {
this.listId = options.id;
this.loadQuote();
},
methods: {
formatMoney(n) {
if (n == null || n === '') {
return '--';
}
const num = Number(n) || 0;
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2, minimumFractionDigits: 0 });
},
itemCover(item) {
return item.cover || (item.product && item.product.cover) || '';
},
itemTitle(item) {
return item.title || (item.product && item.product.title) || '-';
},
itemIdentifier(item) {
return item.identifier || (item.product && item.product.identifier) || '';
},
loadQuote() {
uni.showLoading({ title: '加载中...' });
getCartItemApi(this.listId).then((res) => {
const data = res || { items: [] };
this.info = data;
this.listName = data.name || '-';
this.quote_final_total = data.quote_final_total;
this.quote_items_total = data.quote_items_total;
this.quote_preferential_total = data.quote_preferential_total;
uni.hideLoading();
});
},
},
};
</script>
<style lang="scss">
view, scroll-view, text, image {
box-sizing: border-box;
}
.page {
min-height: 100vh;
background: #f3f1ec;
}
.page-scroll {
min-height: 100vh;
padding: 24rpx 24rpx 48rpx;
}
.quote-sheet {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 12rpx 40rpx rgba(31, 111, 91, 0.08);
border: 1rpx solid rgba(31, 111, 91, 0.12);
}
.sheet-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 36rpx 32rpx 28rpx;
border-bottom: 4rpx solid #1f6f5b;
background: #f7f5f1;
}
.brand-block {
display: flex;
align-items: center;
gap: 20rpx;
}
.brand-mark {
width: 12rpx;
height: 72rpx;
border-radius: 6rpx;
background: linear-gradient(180deg, #1f6f5b 0%, #c4a574 100%);
}
.brand-text {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.brand-name {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
letter-spacing: 1rpx;
}
.brand-sub {
font-size: 18rpx;
color: #1f6f5b;
letter-spacing: 4rpx;
opacity: 0.85;
}
.header-right {
text-align: right;
flex: 1;
padding-left: 24rpx;
}
.sheet-title {
display: block;
font-size: 52rpx;
font-weight: 400;
color: #1a1a1a;
font-family: serif;
letter-spacing: 8rpx;
margin-bottom: 8rpx;
}
.sheet-list-name {
font-size: 24rpx;
color: #6d655c;
}
.meta-grid {
display: flex;
flex-wrap: wrap;
padding: 24rpx 32rpx;
gap: 16rpx 24rpx;
background: #faf9f7;
border-bottom: 1rpx solid #ebe6df;
}
.meta-item {
width: calc(50% - 12rpx);
display: flex;
flex-direction: column;
gap: 6rpx;
}
.meta-label {
font-size: 22rpx;
color: #9a9288;
}
.meta-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.meta-value.accent {
color: #1f6f5b;
font-weight: 600;
}
.items-section {
padding: 8rpx 0;
}
.quote-row {
display: flex;
align-items: flex-start;
gap: 20rpx;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0ebe4;
background: #fff;
}
.quote-row:nth-child(even) {
background: #fcfbfa;
}
.row-index {
width: 40rpx;
font-size: 24rpx;
color: #1f6f5b;
font-weight: 700;
flex-shrink: 0;
padding-top: 8rpx;
}
.row-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background: #f3f1ec;
flex-shrink: 0;
border: 1rpx solid #ebe6df;
}
.row-body {
flex: 1;
min-width: 0;
}
.row-title {
font-size: 30rpx;
color: #222;
font-weight: 500;
line-height: 1.4;
display: block;
margin-bottom: 8rpx;
}
.row-id {
font-size: 22rpx;
color: #999;
display: block;
margin-bottom: 12rpx;
}
.sku-block {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-bottom: 16rpx;
padding: 12rpx 16rpx;
background: rgba(31, 111, 91, 0.08);
border-radius: 8rpx;
border-left: 4rpx solid #1f6f5b;
}
.sku-tag {
font-size: 20rpx;
color: #fff;
background: #1f6f5b;
padding: 4rpx 10rpx;
border-radius: 4rpx;
flex-shrink: 0;
font-weight: 600;
}
.sku-text {
font-size: 24rpx;
color: #444;
line-height: 1.5;
flex: 1;
}
.row-prices {
border-top: 1rpx dashed #e8e4dc;
padding-top: 16rpx;
}
.price-line {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6rpx 0;
}
.price-line.final {
margin-top: 8rpx;
padding-top: 12rpx;
border-top: 1rpx solid #f0ebe4;
}
.pl-label {
font-size: 24rpx;
color: #999;
}
.pl-val {
font-size: 26rpx;
color: #666;
font-family: 'Times New Roman', serif;
}
.pl-val.accent {
color: #B4854D;
}
.pl-val.final-val {
font-size: 32rpx;
font-weight: bold;
color: #1f6f5b;
}
.empty-hint {
text-align: center;
padding: 80rpx 0;
font-size: 28rpx;
color: #ccc;
}
.total-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16rpx;
padding: 28rpx 32rpx;
background: #f7f5f1;
border-top: 2rpx solid #1f6f5b;
}
.total-left {
font-size: 24rpx;
color: #6d655c;
flex: 1;
min-width: 200rpx;
}
.total-right {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.total-label {
font-size: 26rpx;
color: #444;
}
.total-amount {
font-size: 44rpx;
font-weight: 700;
color: #1f6f5b;
font-family: 'Times New Roman', serif;
letter-spacing: 1rpx;
}
.sheet-footer {
padding: 24rpx 32rpx 36rpx;
background: #fff;
}
.footer-note {
font-size: 22rpx;
color: #7a7268;
line-height: 1.7;
}
</style>

View File

@@ -67,6 +67,7 @@
</view>
<!-- 更精致的文字区域 -->
<view class="category-info">
<text class="category-name-en">{{ item.name_en || '-' }}</text>
<text class="category-name">{{ item.name }}</text>
<view class="category-indicator"></view>
</view>
@@ -316,6 +317,18 @@ export default {
max-width: 100%;
}
.category-name-en {
font-size: 20rpx;
color: #B4854D;
letter-spacing: 1rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
margin-bottom: 4rpx;
text-transform: uppercase;
}
/* 卡片文字下方的点缀,增加设计感 */
.category-indicator {
width: 24rpx;

View File

@@ -99,6 +99,36 @@
</view>
</up-popup>
<!-- 完善资料弹窗 -->
<u-modal
:show="profileModal"
title="完善资料"
:showCancelButton="true"
cancelText="稍后"
confirmText="确认"
confirmColor="#B4854D"
@confirm="confirmProfile"
@cancel="profileModal = false"
>
<view class="profile-modal-content">
<button class="avatar-picker" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image
v-if="profileForm.avatar"
:src="profileForm.avatar"
mode="aspectFill"
class="avatar-preview"
/>
<text v-else class="avatar-placeholder">选择头像</text>
</button>
<input
type="nickname"
class="nickname-input"
v-model="profileForm.nick_name"
placeholder="请输入昵称"
/>
</view>
</u-modal>
<!-- 底部垫高 -->
<view class="safe-padding-bottom"></view>
@@ -119,9 +149,10 @@
</template>
<script>
import { getProductDetailApi } from "@/api/page/product";
import { getProductDetailApi, productViewApi } from "@/api/page/product";
import { getCartListApi, toCartApi } from "@/api/page/cart";
import {getCache} from "@/utils/cache";
import { updateProfileApi } from "@/api/page/user";
import { getCache, setCache } from "@/utils/cache";
import { urlSafeBase64Encode } from "@/utils/utils";
export default {
@@ -130,6 +161,11 @@ export default {
id: 0,
p_user_id: 0,
listModal: false,
profileModal: false,
profileForm: {
nick_name: '',
avatar: ''
},
listOption: [],
product: {
title: '',
@@ -187,6 +223,39 @@ export default {
cover: res.cover || res.carousel[0] || ''
};
this.initAlias();
productViewApi(this.id).catch(() => {});
this.checkProfile();
});
},
checkProfile() {
const userInfo = getCache('user_info') || {};
const nick = userInfo.nick_name || '';
const needProfile = !userInfo.avatar || !nick || nick.indexOf('微信用户') === 0;
if (needProfile) {
this.profileForm = {
nick_name: nick,
avatar: userInfo.avatar || ''
};
this.profileModal = true;
}
},
onChooseAvatar(e) {
this.profileForm.avatar = e.detail.avatarUrl;
},
confirmProfile() {
if (!this.profileForm.nick_name || !this.profileForm.avatar) {
uni.showToast({ title: '请完善头像和昵称', icon: 'none' });
return;
}
updateProfileApi(this.profileForm.nick_name, this.profileForm.avatar).then(() => {
const userInfo = getCache('user_info') || {};
setCache('user_info', {
...userInfo,
nick_name: this.profileForm.nick_name,
avatar: this.profileForm.avatar
});
this.profileModal = false;
uni.showToast({ title: '资料已更新', icon: 'success' });
});
},
initAlias() {
@@ -468,4 +537,48 @@ export default {
align-items: center;
justify-content: center;
}
.profile-modal-content {
padding: 20rpx 10rpx 10rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 30rpx;
width: 100%;
}
.avatar-picker {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
overflow: hidden;
background: #F7F8FA;
border: 2rpx dashed #B4854D;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.avatar-picker::after {
border: none;
}
.avatar-preview {
width: 100%;
height: 100%;
}
.avatar-placeholder {
font-size: 24rpx;
color: #B4854D;
}
.nickname-input {
width: 100%;
text-align: center;
font-size: 28rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #eee;
}
</style>

View File

@@ -37,7 +37,10 @@
:class="['tab-item', { active: currentTab === index }]"
@tap="switchTab(index, tab.id)"
>
<text class="tab-text">{{ tab.name }}</text>
<view class="tab-text-wrap">
<text class="tab-text-en">{{ tab.name_en || '-' }}</text>
<text class="tab-text">{{ tab.name }}</text>
</view>
<view class="active-dot" v-if="currentTab === index"></view>
</view>
</scroll-view>
@@ -205,6 +208,7 @@ export default {
res.unshift({
id: 0,
name: "全部",
name_en: "All",
})
this.tabs = res
})
@@ -360,16 +364,38 @@ export default {
.category-tabs {
white-space: nowrap;
padding: 0 10rpx;
height: 90rpx;
height: 110rpx;
}
.tab-item {
display: inline-block;
padding: 0 30rpx;
height: 90rpx;
line-height: 90rpx;
padding: 0 28rpx;
height: 110rpx;
position: relative;
transition: all 0.3s ease;
vertical-align: top;
}
.tab-text-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding-bottom: 12rpx;
box-sizing: border-box;
}
.tab-text-en {
font-size: 18rpx;
color: #999;
line-height: 1.2;
margin-bottom: 4rpx;
text-transform: uppercase;
}
.tab-item.active .tab-text-en {
color: #B4854D;
}
.tab-text {
@@ -377,6 +403,7 @@ export default {
color: #666;
font-weight: 400;
transition: all 0.3s;
line-height: 1.2;
}
.tab-item.active .tab-text {
@@ -387,7 +414,7 @@ export default {
.active-dot {
position: absolute;
bottom: 16rpx;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 8rpx;