Browse Source

Merge branch 'master' into fxw

付晓文。 1 month ago
parent
commit
8d5d094538

+ 65 - 4
api/memberPrepaid.js

@@ -9,6 +9,51 @@ export function getMerchantId() {
 	return info.merchantId || ''
 }
 
+/** 获取本商户经纬度(共享门店列表按门店坐标算距离) */
+export function getMerchantLocation(merchantId = getMerchantId()) {
+	if (!merchantId) {
+		return Promise.reject(new Error('未获取到门店信息'))
+	}
+	return http
+		.get('/merchant/localMerchantInfo/queryById', {
+			params: { id: merchantId }
+		})
+		.then((res) => {
+			if (res.data.code !== 200 || !res.data.result) {
+				return Promise.reject(new Error(res.data.message || '获取门店信息失败'))
+			}
+			const { longitude, latitude } = res.data.result
+			if (
+				longitude === undefined ||
+				longitude === null ||
+				longitude === '' ||
+				latitude === undefined ||
+				latitude === null ||
+				latitude === ''
+			) {
+				return Promise.reject(new Error('门店未配置经纬度'))
+			}
+			return {
+				longitude: Number(longitude),
+				latitude: Number(latitude)
+			}
+		})
+}
+
+/** 格式化门店距离展示,如 746.00m / 3.24km */
+export function formatStoreDistance(item = {}) {
+	if (item.distance == null || item.distance === '') {
+		return item.distanceText || ''
+	}
+	const num = Number(item.distance)
+	if (Number.isNaN(num)) {
+		return String(item.distanceText || item.distance)
+	}
+	const text = Number.isInteger(num) ? String(num) : num.toFixed(2)
+	const unit = item.distanceUnit || ''
+	return unit ? `${text}${unit}` : text
+}
+
 /**
  * auditStatus: 0未申请 / 1待审核 / 2已通过 / 3已驳回
  * operationStatus: 0关闭 / 1开启
@@ -55,17 +100,33 @@ const api = {
 		return http.get(`${BASE}/store/current`, { params: { merchantId } })
 	},
 
-	/** 可添加共享门店分页 */
+	/**
+	 * 可添加共享门店分页
+	 * 必传 longitude / latitude,按距离由近到远
+	 */
 	getShareableStores(params = {}) {
 		return http.get(`${BASE}/store/shareable/page`, {
-			params: { merchantId: getMerchantId(), pageNo: 1, pageSize: 10, ...params }
+			params: {
+				merchantId: getMerchantId(),
+				pageNo: 1,
+				pageSize: 10,
+				...params
+			}
 		})
 	},
 
-	/** 已共享门店分页 */
+	/**
+	 * 已共享门店分页
+	 * 必传 longitude / latitude,按距离由近到远
+	 */
 	getSharedStores(params = {}) {
 		return http.get(`${BASE}/store/shared/page`, {
-			params: { merchantId: getMerchantId(), pageNo: 1, pageSize: 10, ...params }
+			params: {
+				merchantId: getMerchantId(),
+				pageNo: 1,
+				pageSize: 10,
+				...params
+			}
 		})
 	},
 

+ 90 - 36
pages/merchantDish/dish/sale-time.vue

@@ -72,12 +72,17 @@
 		<view class="pick-mask" v-if="monthPickerShow" @click="monthPickerShow = false">
 			<view class="pick-sheet" @click.stop>
 				<view class="pick-title">选择月份</view>
-				<picker-view class="pick-view" :value="monthPickerValue" @change="onMonthPickerChange">
+				<picker-view
+					class="pick-view"
+					:indicator-style="pickerIndicatorStyle"
+					:value="monthPickerValue"
+					@change="onMonthPickerChange"
+				>
 					<picker-view-column>
-						<view class="pick-item" v-for="y in yearList" :key="y">{{ y }}年</view>
+						<view class="pick-item" :style="pickerItemStyle" v-for="y in yearList" :key="y">{{ y }}年</view>
 					</picker-view-column>
 					<picker-view-column>
-						<view class="pick-item" v-for="m in 12" :key="m">{{ m }}月</view>
+						<view class="pick-item" :style="pickerItemStyle" v-for="m in 12" :key="m">{{ m }}月</view>
 					</picker-view-column>
 				</picker-view>
 				<view class="pick-confirm" @click="confirmMonthPicker">确定</view>
@@ -95,6 +100,7 @@
 					>
 						{{ formatCnDate(tempStart) || '开始日期' }}
 					</view>
+					<text class="range-sep">-</text>
 					<view
 						:class="['preview-box', rangeField === 'end' ? 'on' : '']"
 						@click="switchRangeField('end')"
@@ -102,15 +108,21 @@
 						{{ formatCnDate(tempEnd) || '结束日期' }}
 					</view>
 				</view>
-				<picker-view class="pick-view" :value="datePickerValue" @change="onDatePickerChange">
+				<picker-view
+					:key="rangePickerKey"
+					class="pick-view"
+					:indicator-style="pickerIndicatorStyle"
+					:value="datePickerValue"
+					@change="onDatePickerChange"
+				>
 					<picker-view-column>
-						<view class="pick-item" v-for="y in yearList" :key="'dy' + y">{{ y }}年</view>
+						<view class="pick-item" :style="pickerItemStyle" v-for="y in yearList" :key="'dy' + y">{{ y }}年</view>
 					</picker-view-column>
 					<picker-view-column>
-						<view class="pick-item" v-for="m in 12" :key="'dm' + m">{{ m }}月</view>
+						<view class="pick-item" :style="pickerItemStyle" v-for="m in 12" :key="'dm' + m">{{ m }}月</view>
 					</picker-view-column>
 					<picker-view-column>
-						<view class="pick-item" v-for="d in dayList" :key="'dd' + d">{{ d }}日</view>
+						<view class="pick-item" :style="pickerItemStyle" v-for="d in dayList" :key="'dd' + d">{{ d }}日</view>
 					</picker-view-column>
 				</picker-view>
 				<view class="pick-confirm" @click="confirmRangePicker">确定</view>
@@ -134,6 +146,8 @@ export default {
 		const year = now.getFullYear();
 		const yearList = [];
 		for (let y = year - 2; y <= year + 10; y++) yearList.push(y);
+		// picker-view 的 indicator-style 只认 px;与选项行高必须一致,否则会视觉错位选错
+		const itemPx = uni.upx2px(80);
 		return {
 			fromMulti: false,
 			mode: 'fixed',
@@ -149,10 +163,13 @@ export default {
 			monthPickerValue: [2, now.getMonth()],
 			rangePickerShow: false,
 			rangeField: 'start',
+			rangePickerKey: 0,
 			tempStart: '',
 			tempEnd: '',
 			datePickerValue: [2, now.getMonth(), now.getDate() - 1],
-			pickerDay: now.getDate()
+			pickerDay: now.getDate(),
+			pickerIndicatorStyle: `height: ${itemPx}px;`,
+			pickerItemStyle: `height: ${itemPx}px; line-height: ${itemPx}px;`
 		};
 	},
 	computed: {
@@ -293,19 +310,33 @@ export default {
 			const base = this.normalizeDate(
 				field === 'end' ? this.endDate || this.startDate : this.startDate || this.endDate
 			);
-			this.applyDateToPicker(base);
-			// 首次打开且对应字段为空时,用当前滚轮日期预填
 			const now = new Date();
 			const fallback = this.toYmd(now.getFullYear(), now.getMonth() + 1, now.getDate());
 			const seed = base || fallback;
 			if (field === 'start' && !this.tempStart) this.tempStart = seed;
 			if (field === 'end' && !this.tempEnd) this.tempEnd = seed;
+			this.remountRangePicker(
+				this.normalizeDate(field === 'end' ? this.tempEnd : this.tempStart) || seed
+			);
 			this.rangePickerShow = true;
 		},
 		switchRangeField(field) {
+			if (this.rangeField === field) return;
+			// 切换前先把当前滚轮值写回当前字段,避免开始/结束串值
+			this.syncPickerToField();
 			this.rangeField = field;
 			const base = this.normalizeDate(field === 'start' ? this.tempStart : this.tempEnd);
-			this.applyDateToPicker(base);
+			const now = new Date();
+			const fallback = this.toYmd(now.getFullYear(), now.getMonth() + 1, now.getDate());
+			const seed = base || fallback;
+			if (field === 'start' && !this.tempStart) this.tempStart = seed;
+			if (field === 'end' && !this.tempEnd) this.tempEnd = seed;
+			this.remountRangePicker(seed);
+		},
+		/** 强制重建 picker-view,避免日列天数变化后索引错位 */
+		remountRangePicker(ymd) {
+			this.rangePickerKey += 1;
+			this.applyDateToPicker(ymd);
 		},
 		applyDateToPicker(ymd) {
 			const now = new Date();
@@ -322,39 +353,56 @@ export default {
 			if (yi < 0) yi = 0;
 			const total = new Date(y, m, 0).getDate();
 			if (d > total) d = total;
+			if (d < 1) d = 1;
 			this.pickerDay = d;
-			this.datePickerValue = [yi, m - 1, d - 1];
+			const next = [yi, m - 1, d - 1];
+			this.datePickerValue = next;
 			this.$nextTick(() => {
-				this.datePickerValue = [yi, m - 1, d - 1];
+				this.datePickerValue = next.slice();
 			});
 		},
-		onDatePickerChange(e) {
-			const val = e.detail.value || [0, 0, 0];
-			const y = this.yearList[val[0]] || this.year;
-			const m = (val[1] || 0) + 1;
+		parsePickerValue(val) {
+			const raw = Array.isArray(val) ? val : [0, 0, 0];
+			const yi = Math.max(0, Number(raw[0]) || 0);
+			const mi = Math.max(0, Number(raw[1]) || 0);
+			const y = this.yearList[yi] || this.year;
+			const m = mi + 1;
 			const total = new Date(y, m, 0).getDate();
-			let d = (val[2] || 0) + 1;
+			let d = Math.max(0, Number(raw[2]) || 0) + 1;
 			if (d > total) d = total;
-			this.pickerDay = d;
-			this.datePickerValue = [val[0], val[1], d - 1];
+			if (d < 1) d = 1;
+			return { yi, mi, y, m, d, total };
+		},
+		syncPickerToField() {
+			const { y, m, d } = this.parsePickerValue(this.datePickerValue);
 			const ymd = this.toYmd(y, m, d);
 			if (this.rangeField === 'start') this.tempStart = ymd;
 			else this.tempEnd = ymd;
+			return ymd;
 		},
-		confirmRangePicker() {
-			// 滚轮停住后若未触发 change,用当前索引兜底写入当前字段
-			const [yi, mi, di] = this.datePickerValue;
-			const y = this.yearList[yi] || this.year;
-			const m = (mi || 0) + 1;
-			const total = new Date(y, m, 0).getDate();
-			let d = (di || 0) + 1;
-			if (d > total) d = total;
-			const ymd = this.toYmd(y, m, d);
-			if (this.rangeField === 'start') {
-				if (!this.tempStart) this.tempStart = ymd;
-			} else if (!this.tempEnd) {
-				this.tempEnd = ymd;
+		onDatePickerChange(e) {
+			const prev = this.datePickerValue || [0, 0, 0];
+			const { yi, mi, y, m, d, total } = this.parsePickerValue(e.detail.value);
+			const dayIdx = Math.min(d - 1, total - 1);
+			const next = [yi, mi, dayIdx];
+			this.pickerDay = dayIdx + 1;
+			this.datePickerValue = next;
+
+			const ymd = this.toYmd(y, m, dayIdx + 1);
+			if (this.rangeField === 'start') this.tempStart = ymd;
+			else this.tempEnd = ymd;
+
+			// 年/月变化会导致日列长度变化,需重建,否则日列视觉与索引错位
+			if (prev[0] !== yi || prev[1] !== mi) {
+				this.rangePickerKey += 1;
+				this.$nextTick(() => {
+					this.datePickerValue = next.slice();
+				});
 			}
+		},
+		confirmRangePicker() {
+			// 始终以当前滚轮为准写回当前字段,避免未触发 change 时显示与提交不一致
+			this.syncPickerToField();
 
 			if (!this.tempStart || !this.tempEnd) {
 				uni.showToast({ title: '请选择开始和结束日期', icon: 'none' });
@@ -707,11 +755,13 @@ export default {
 }
 .range-preview {
 	display: flex;
-	gap: 20rpx;
+	align-items: center;
+	gap: 16rpx;
 	margin-bottom: 12rpx;
 }
 .preview-box {
 	flex: 1;
+	min-width: 0;
 	height: 72rpx;
 	line-height: 70rpx;
 	text-align: center;
@@ -721,6 +771,10 @@ export default {
 	border: 2rpx solid #e5e5e5;
 	border-radius: 12rpx;
 	box-sizing: border-box;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	padding: 0 8rpx;
 	&.on {
 		color: #449aff;
 		border-color: #449aff;
@@ -732,11 +786,11 @@ export default {
 	width: 100%;
 }
 .pick-item {
-	height: 80rpx;
-	line-height: 80rpx;
+	/* 具体 height/line-height 由 pickerItemStyle(px)控制,与 indicator 对齐 */
 	text-align: center;
 	font-size: 30rpx;
 	color: #333;
+	box-sizing: border-box;
 }
 .pick-confirm {
 	margin-top: 12rpx;

+ 28 - 1
pages/product/creatProduct.vue

@@ -662,6 +662,10 @@
 			selectProductType() {
 				if (this.tabIndex == '0' || this.form.isUpdatePrice == 1) {
 					this.isSelectType = true
+					// 编辑模式下已有分类数据时,保留默认选中
+					if (this.productId && this.form.categoryId) {
+						return
+					}
 					this.showText = ''
 					this.productType = []
 					this.selectTypeList = []
@@ -691,7 +695,6 @@
 			},
 			//选中二级
 			selectRight(item, index) {
-				this.getRightTree(this.treeCenter[0].id)
 				this.currentCenter = index
 				this.currentRight = null
 				if (this.selectTypeList.length >= 2) {
@@ -832,6 +835,22 @@
 
 				}
 			},
+			// 根据接口返回的分类ID,级联加载并选中树节点
+			async initDefaultCategory() {
+				if (!this.productType.length) return;
+				// 一级
+				this.currentLeft = this.treeLeft.findIndex(item => item.id === this.productType[0]);
+				if (this.currentLeft === -1 || this.productType.length < 2) return;
+				// 二级
+				const res1 = await this.$http.get("/sys/category/childList?pid=" + this.productType[0]);
+				this.treeCenter = res1.data.result || [];
+				this.currentCenter = this.treeCenter.findIndex(item => item.id === this.productType[1]);
+				if (this.currentCenter === -1 || this.productType.length < 3) return;
+				// 三级
+				const res2 = await this.$http.get("/sys/category/childList?pid=" + this.productType[1]);
+				this.treeRight = res2.data.result || [];
+				this.currentRight = this.treeRight.findIndex(item => item.id === this.productType[2]);
+			},
 			//回显数据
 			getForm() {
 				this.$http.get("/app/product/queryById?id=" + this.productId, {}).then(res => {
@@ -841,6 +860,14 @@
 						this.jifenChecked = res.data.result.integralAward ? true : false
 						this.jindouChecked = res.data.result.goldAward ? true : false
 						this.showText = res.data.result.productCategory
+						// 设置分类树的默认选中
+						if (res.data.result.categoryId) {
+							this.productType = res.data.result.categoryId.split(',').filter(id => id)
+						}
+						if (res.data.result.productCategory) {
+							this.selectTypeList = res.data.result.productCategory.split('·').filter(name => name)
+						}
+						this.initDefaultCategory()
 						this.fmImageList.push({
 							url: res.data.result.productImage
 						})

+ 15 - 2
pages/product/details.vue

@@ -39,7 +39,7 @@
 						</view>
 					</view>
 				</view>
-				<rich-text :nodes="form.richText" v-if="form.richText"></rich-text>
+				<rich-text class="rich_text" :nodes="form.richText" v-if="form.richText"></rich-text>
 
 				<view class="data"  v-if="form.categoryId != '1945013773786701826'">
 					<span class="span">原价</span>{{form.price}}
@@ -327,6 +327,19 @@
 			}
 		}
 
+		.rich_text {
+			display: block;
+			width: 100%;
+			overflow: hidden;
+
+			::v-deep img {
+				display: block;
+				width: 100% !important;
+				max-width: 100% !important;
+				height: auto !important;
+			}
+		}
+
 		.particulars {
 			width: 100%;
 			margin-top: 20rpx;
@@ -424,4 +437,4 @@
 			}
 		}
 	}
-</style>
+</style>

+ 25 - 9
pages/topUp/agreement.vue

@@ -89,16 +89,19 @@
 				submitting: false,
 				agreementVersion: '',
 				agreementTitle: '储值功能开通业务规则',
-				contentText: ''
+				contentText: '',
+				isReapply: false
 			};
 		},
-		onLoad() {
+		onLoad(options = {}) {
+			this.isReapply = String(options.reapply || '') === '1';
 			this.loadAgreement();
 			this.checkStatus();
 		},
 		onReady() {
 			const sys = uni.getSystemInfoSync();
-			this.scrollHeight = `${sys.windowHeight - uni.upx2px(280) - (sys.statusBarHeight || 0) - 44}px`;
+			// 底部固定栏(协议勾选 + 申请按钮 + 安全区)预留高度,避免末尾条款被遮挡
+			this.scrollHeight = `${sys.windowHeight - uni.upx2px(320) - (sys.statusBarHeight || 0) - 44}px`;
 		},
 		methods: {
 			checkStatus() {
@@ -106,7 +109,17 @@
 				if (!merchantId) return;
 				prepaidApi.getFeatureStatus(merchantId).then((res) => {
 					if (res.data.code !== 200) return;
-					const auditStatus = Number((res.data.result || {}).auditStatus);
+					const data = res.data.result || {};
+					const auditStatus = Number(data.auditStatus);
+					const reapplyStatus = Number(data.reapplyStatus);
+					// 驳回后重新申请:允许停留在申请页,避免又跳回驳回页
+					if (this.isReapply && auditStatus === 3) {
+						if (reapplyStatus === 1) {
+							uni.showToast({ title: '当前不可重新申请', icon: 'none' });
+							uni.redirectTo({ url: '/pages/topUp/status' });
+						}
+						return;
+					}
 					if (auditStatus === 1 || auditStatus === 3) {
 						uni.redirectTo({ url: '/pages/topUp/status' });
 					} else if (auditStatus === 2) {
@@ -116,12 +129,14 @@
 			},
 			loadAgreement() {
 				prepaidApi.getCurrentAgreement().then((res) => {
-					console.log('11111',res);
 					if (res.data.code !== 200 || !res.data.result) return;
 					const data = res.data.result;
 					this.agreementVersion = data.agreementVersion || '';
 					this.agreementTitle = data.agreementTitle || '储值功能开通业务规则';
-					this.contentText = data.contentText || '';
+					const apiText = (data.contentText || '').trim();
+					// 接口正文若缺少末条争议解决条款,回退本地完整协议,避免展示截断
+					const requiredClause = '因本协议产生争议';
+					this.contentText = apiText && apiText.includes(requiredClause) ? apiText : '';
 				});
 			},
 			buildIdempotencyKey() {
@@ -174,7 +189,7 @@
 
 <style scoped lang="scss">
 	.page {
-		min-height: 100vh;
+		// min-height: 100vh;
 		background: #f7f7f7;
 		display: flex;
 		flex-direction: column;
@@ -182,15 +197,16 @@
 
 	.content {
 		flex: 1;
-		padding: 24rpx 24rpx 0;
+		padding: 24rpx 24rpx 48rpx;
 		box-sizing: border-box;
+		padding-bottom: 220rpx;
 	}
 
 	.card {
 		background: #fff;
 		border-radius: 16rpx;
 		padding: 32rpx 28rpx 40rpx;
-		margin-bottom: 24rpx;
+		// margin-bottom: 24rpx;
 	}
 
 	.doc-title {

+ 9 - 5
pages/topUp/recordDetail.vue

@@ -110,7 +110,7 @@
 							<image class="coupon-bg" src="/static/topUp/coupon.png" mode="scaleToFill"></image>
 							<view class="coupon-body">
 								<view class="coupon-top">
-									<text class="c-name">{{ c.name }}</text>
+									<text class="c-name">{{ formatCouponName(c.name) }}</text>
 									<text class="c-value">{{ c.display }}</text>
 								</view>
 								<view class="coupon-bottom">
@@ -170,6 +170,10 @@
 				const n = Number(val) || 0;
 				return Number.isInteger(n) ? String(n) : n.toFixed(2);
 			},
+			formatCouponName(name) {
+				const s = String(name || '');
+				return s.length > 6 ? `${s.slice(0, 6)}...` : s;
+			},
 			loadDetail() {
 				if (!this.recordId) {
 					uni.showToast({ title: '缺少明细ID', icon: 'none' });
@@ -191,11 +195,11 @@
 							return {
 								name: c.couponName || '',
 								display: isDiscount
-									? c.faceAmount != null
-										? `${c.faceAmount}折`
+									? c.contentInfo != null
+										? `${c.contentInfo}折`
 										: '折扣'
-									: c.faceAmount != null
-										? `${c.faceAmount}元`
+									: c.contentInfo != null
+										? `${c.contentInfo}元`
 										: '优惠',
 								qty: Number(c.quantity) || 1
 							};

+ 140 - 43
pages/topUp/settings.vue

@@ -12,15 +12,15 @@
 		<view class="card min-card">
 			<view class="min-head">
 				<text class="label">自定义充值最低金额</text>
-				<text class="min-tip">默认最低金额为100</text>
+				<text class="min-tip">默认为固定档位中的最低金额</text>
 			</view>
 			<view class="min-input-wrap">
 				<text class="currency">¥</text>
 				<input
 					class="min-input"
-					type="digit"
+					type="number"
 					v-model="form.minAmount"
-					placeholder="100"
+					:placeholder="String(lowestTierAmount || '')"
 					placeholder-class="placeholder"
 					@input="onMinAmountInput"
 				/>
@@ -94,27 +94,29 @@
 						<text class="form-label">充值金额</text>
 						<input
 							class="form-input"
-							type="digit"
+							type="number"
 							v-model="tierForm.amount"
-							placeholder="请点击输入"
+							placeholder="请输入正整数"
 							placeholder-class="form-placeholder"
+							@input="onTierAmountInput('amount', $event)"
 						/>
 					</view>
 					<view class="form-row">
 						<text class="form-label">赠送金额</text>
 						<input
 							class="form-input"
-							type="digit"
+							type="number"
 							v-model="tierForm.giftAmount"
-							placeholder="请点击输入"
+							placeholder="选填,默认0"
 							placeholder-class="form-placeholder"
+							@input="onTierAmountInput('giftAmount', $event)"
 						/>
 					</view>
 					<view class="form-row link-row" @click="openCouponSelect">
 						<text class="form-label">赠送优惠券</text>
 						<view class="link-right">
 							<text class="link-text" :class="{ active: couponSelectedCount > 0 }">
-								{{ couponSelectedCount > 0 ? '已选择' : '请选择' }}
+								{{ couponSelectedCount > 0 ? `已选 ${couponSelectedCount} 张` : '去选择' }}
 							</text>
 							<u-icon name="arrow-right" color="#c0c4cc" size="14"></u-icon>
 						</view>
@@ -214,12 +216,8 @@
 				</view>
 
 				<view class="footer coupon-footer">
-					<view
-						class="save-btn"
-						:class="{ disabled: tempCouponCount <= 0 }"
-						@click="confirmCouponSelect"
-					>
-						{{ tempCouponCount > 0 ? `确定添加${tempCouponCount}张` : '确认添加' }}
+					<view class="save-btn" @click="confirmCouponSelect">
+						{{ tempCouponCount > 0 ? `确定选择(已选 ${tempCouponCount} 张)` : '确定选择' }}
 					</view>
 				</view>
 			</view>
@@ -237,6 +235,14 @@
 		return `t_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
 	}
 
+	/** 默认固定档位:档位1金额100、档位2金额200,赠送与优惠券均为0 */
+	function createDefaultTiers() {
+		return [
+			{ id: createId(), amount: 100, giftAmount: 0, coupons: [] },
+			{ id: createId(), amount: 200, giftAmount: 0, coupons: [] }
+		];
+	}
+
 	function formatValidText(beginTime, endTime) {
 		if (!beginTime && !endTime) return '长期有效';
 		const begin = beginTime ? String(beginTime).slice(0, 10) : '';
@@ -245,6 +251,51 @@
 		return begin || end || '长期有效';
 	}
 
+	function isPlainAmount(val) {
+		return /^[\d.]+$/.test(String(val || '').trim());
+	}
+
+	function pickAmount(val) {
+		const matched = String(val || '').match(/[\d.]+/);
+		return matched ? matched[0] : '';
+	}
+
+	/** 折扣券 / 满减券左侧面额与使用条件文案(接口多为纯数字,需补「元」「折」) */
+	function formatCouponTexts(item) {
+		const isDiscount = String(item.couponType) === '2';
+		const info = String(item.contentInfo || '').trim();
+		const cond = String(item.contentConditions || '').trim();
+		const infoNum = isPlainAmount(info) ? info : pickAmount(info);
+		const condNum = isPlainAmount(cond) ? cond : pickAmount(cond);
+		const noThreshold = !condNum || condNum === '0';
+
+		if (isDiscount) {
+			const display = infoNum ? `${infoNum}折` : info || '折扣';
+			const condition = noThreshold
+				? '不限制条件使用'
+				: `满${condNum}元可使用`;
+			return { display, condition };
+		}
+
+		// 满减:左侧「满X元 / 减Y元」
+		let display = '';
+		if (infoNum && condNum) {
+			display = `满${condNum}元\n减${infoNum}元`;
+		} else if (infoNum) {
+			display = `${infoNum}元`;
+		} else if (info) {
+			display = info
+				.replace(/满\s*([\d.]+)\s*元?/g, '满$1元')
+				.replace(/减\s*([\d.]+)\s*元?/g, '减$1元');
+		} else {
+			display = '优惠';
+		}
+		const condition = noThreshold
+			? '不限制条件使用'
+			: `满${condNum}元可抵扣`;
+		return { display, condition };
+	}
+
 	export default {
 		components: {
 			ListScrollView
@@ -257,7 +308,7 @@
 				form: {
 					acceptShare: true,
 					minAmount: '100',
-					tiers: []
+					tiers: createDefaultTiers()
 				},
 				tierVisible: false,
 				tierEditIndex: -1,
@@ -284,9 +335,17 @@
 					return gift > 0 || couponQty > 0;
 				});
 			},
+			/** 固定档位中的最低充值金额,用作自定义最低金额默认值 */
+			lowestTierAmount() {
+				const amounts = (this.form.tiers || [])
+					.map((t) => Number(t.amount))
+					.filter((n) => !Number.isNaN(n) && n > 0);
+				if (!amounts.length) return 0;
+				return Math.min(...amounts);
+			},
 			canSubmitTier() {
 				const amount = String(this.tierForm.amount || '').trim();
-				return !!amount && Number(amount) > 0;
+				return !!amount && /^\d+$/.test(amount) && Number(amount) > 0;
 			},
 			tierFormTitle() {
 				if (this.tierEditIndex === -1) {
@@ -319,6 +378,13 @@
 					this.form.minAmount = val;
 				});
 			},
+			onTierAmountInput(field, e) {
+				const val = String((e && e.detail && e.detail.value) || this.tierForm[field] || '')
+					.replace(/[^\d]/g, '');
+				this.$nextTick(() => {
+					this.$set(this.tierForm, field, val);
+				});
+			},
 			loadSettings() {
 				this.loading = true;
 				prepaidApi
@@ -331,21 +397,30 @@
 						const data = res.data.result;
 						this.configVersion = data.version;
 						const tiers = Array.isArray(data.tiers) ? data.tiers : [];
+						const mappedTiers = tiers.length
+							? tiers.map((tier) => ({
+									id: tier.tierId || createId(),
+									amount: Number(tier.rechargeAmount) || 0,
+									giftAmount: Number(tier.giftAmount) || 0,
+									coupons: (tier.coupons || []).map((c) => ({
+										id: c.couponId,
+										name: c.couponName || '',
+										type: String(c.couponType) === '2' ? 'discount' : 'reduce',
+										qty: Number(c.quantity) || 1
+									}))
+								}))
+							: createDefaultTiers();
+						const lowest = mappedTiers
+							.map((t) => Number(t.amount))
+							.filter((n) => n > 0);
+						const defaultMin = lowest.length ? Math.min(...lowest) : 100;
 						this.form = {
 							acceptShare: data.acceptSharedPrepaid !== false,
 							minAmount:
-								data.customMinAmount != null ? String(data.customMinAmount) : '100',
-							tiers: tiers.map((tier) => ({
-								id: tier.tierId || createId(),
-								amount: Number(tier.rechargeAmount) || 0,
-								giftAmount: Number(tier.giftAmount) || 0,
-								coupons: (tier.coupons || []).map((c) => ({
-									id: c.couponId,
-									name: c.couponName || '',
-									type: String(c.couponType) === '2' ? 'discount' : 'reduce',
-									qty: Number(c.quantity) || 1
-								}))
-							}))
+								data.customMinAmount != null && data.customMinAmount !== ''
+									? String(Math.floor(Number(data.customMinAmount)) || '')
+									: String(defaultMin),
+							tiers: mappedTiers
 						};
 					})
 					.catch(() => {
@@ -367,8 +442,9 @@
 				const item = this.form.tiers[index];
 				this.tierEditIndex = index;
 				this.tierForm = {
-					amount: item.amount != null ? String(item.amount) : '',
-					giftAmount: item.giftAmount != null ? String(item.giftAmount) : '',
+					amount: item.amount != null ? String(Math.floor(Number(item.amount)) || '') : '',
+					giftAmount:
+						item.giftAmount != null ? String(Math.floor(Number(item.giftAmount)) || 0) : '',
 					coupons: JSON.parse(JSON.stringify(item.coupons || []))
 				};
 				this.tierVisible = true;
@@ -377,6 +453,10 @@
 				this.tierVisible = false;
 			},
 			removeTier(index) {
+				if ((this.form.tiers || []).length <= 2) {
+					uni.showToast({ title: '至少保留2个固定充值档位', icon: 'none' });
+					return;
+				}
 				uni.showModal({
 					title: '提示',
 					content: '确认删除该充值档位吗?',
@@ -391,17 +471,17 @@
 			submitTier() {
 				if (!this.canSubmitTier) return;
 				const amountStr = String(this.tierForm.amount || '').trim();
-				if (!/^\d+(\.\d{1,2})?$/.test(amountStr) || Number(amountStr) <= 0) {
-					uni.showToast({ title: '充值金额须大于0', icon: 'none' });
+				if (!/^\d+$/.test(amountStr) || Number(amountStr) <= 0) {
+					uni.showToast({ title: '充值金额须为正整数', icon: 'none' });
 					return;
 				}
 				const amount = Number(amountStr);
 				const giftRaw = String(this.tierForm.giftAmount || '').trim();
-				const giftAmount = giftRaw === '' ? 0 : Number(giftRaw);
-				if (Number.isNaN(giftAmount) || giftAmount < 0) {
-					uni.showToast({ title: '赠送金额格式不正确', icon: 'none' });
+				if (giftRaw !== '' && !/^\d+$/.test(giftRaw)) {
+					uni.showToast({ title: '赠送金额须为非负整数', icon: 'none' });
 					return;
 				}
+				const giftAmount = giftRaw === '' ? 0 : Number(giftRaw);
 				const payload = {
 					id: this.tierEditIndex === -1 ? createId() : this.form.tiers[this.tierEditIndex].id,
 					amount,
@@ -416,13 +496,12 @@
 				this.tierVisible = false;
 			},
 			mapCouponItem(item) {
-				const type = String(item.couponType);
-				const isDiscount = type === '2';
+				const { display, condition } = formatCouponTexts(item);
 				return {
 					id: item.couponId,
 					name: item.couponName || '',
-					display: item.contentInfo || (isDiscount ? '折扣' : '优惠'),
-					condition: item.contentConditions || '',
+					display,
+					condition,
 					validText: formatValidText(item.beginTime, item.endTime),
 					remainingQuantity: Number(item.remainingQuantity) || 0
 				};
@@ -517,6 +596,12 @@
 					return;
 				}
 				const amounts = this.form.tiers.map((t) => Number(t.amount) || 0);
+				for (let i = 0; i < amounts.length; i++) {
+					if (!Number.isInteger(amounts[i]) || amounts[i] <= 0) {
+						uni.showToast({ title: '档位充值金额须为正整数', icon: 'none' });
+						return;
+					}
+				}
 				for (let i = 1; i < amounts.length; i++) {
 					if (amounts[i] <= amounts[i - 1]) {
 						uni.showToast({ title: '档位充值金额须严格递增', icon: 'none' });
@@ -526,13 +611,24 @@
 				// 有赠送余额/优惠券时接口要求关闭自定义充值
 				const customAmountEnabled = !this.hasGiftConfig;
 				const minRaw = String(this.form.minAmount || '').trim();
-				let customMinAmount = 100;
+				const lowest = this.lowestTierAmount;
+				let customMinAmount = lowest || 0;
 				if (minRaw !== '') {
+					if (!/^\d+$/.test(minRaw) || Number(minRaw) <= 0) {
+						uni.showToast({ title: '自定义最低金额须为正整数', icon: 'none' });
+						return;
+					}
 					customMinAmount = Number(minRaw);
-					if (!customMinAmount || customMinAmount < 100) {
-						uni.showToast({ title: '自定义最低金额不得低于100', icon: 'none' });
+					if (lowest && customMinAmount < lowest) {
+						uni.showToast({
+							title: `自定义最低金额不能低于最低档位(${lowest})`,
+							icon: 'none'
+						});
 						return;
 					}
+				} else if (!lowest) {
+					uni.showToast({ title: '请配置固定充值档位', icon: 'none' });
+					return;
 				}
 				const payload = {
 					acceptSharedPrepaid: !!this.form.acceptShare,
@@ -1046,7 +1142,8 @@
 
 			&.multi {
 				font-size: 26rpx;
-				line-height: 1.35;
+				line-height: 1.45;
+				white-space: pre-line;
 			}
 		}
 

+ 1 - 1
pages/topUp/shareStore.vue

@@ -123,7 +123,7 @@
 					address: item.address || '',
 					rating: Number(item.grade || item.rating || 0),
 					reviewCount: item.commentNum ?? item.reviewCount ?? '',
-					distance: item.distance || item.distanceText || ''
+					distance: formatStoreDistance(item)
 				};
 			},
 			afterFetch(result) {

+ 33 - 7
pages/topUp/shareStoreAdd.vue

@@ -67,7 +67,7 @@
 </template>
 
 <script>
-	import prepaidApi from '@/api/memberPrepaid.js'
+	import prepaidApi, { getMerchantLocation, formatStoreDistance } from '@/api/memberPrepaid.js'
 
 	const DEFAULT_LOGO = '/static/index/shop.png'
 
@@ -86,7 +86,9 @@
 				candidateList: [],
 				selectedIds: [],
 				loading: false,
-				submitting: false
+				submitting: false,
+				longitude: null,
+				latitude: null
 			};
 		},
 		computed: {
@@ -119,13 +121,33 @@
 					address: item.address || '',
 					rating: Number(item.grade || item.rating || 0),
 					reviewCount: item.commentNum ?? item.reviewCount ?? '',
-					distance: item.distance || item.distanceText || ''
+					distance: formatStoreDistance(item)
 				};
 			},
+			ensureLocation() {
+				if (this.longitude != null && this.latitude != null) {
+					return Promise.resolve({
+						longitude: this.longitude,
+						latitude: this.latitude
+					});
+				}
+				return getMerchantLocation().then((loc) => {
+					this.longitude = loc.longitude;
+					this.latitude = loc.latitude;
+					return loc;
+				});
+			},
 			loadCandidates() {
 				this.loading = true;
-				prepaidApi
-					.getShareableStores({ pageNo: 1, pageSize: 100 })
+				this.ensureLocation()
+					.then((loc) =>
+						prepaidApi.getShareableStores({
+							pageNo: 1,
+							pageSize: 100,
+							longitude: loc.longitude,
+							latitude: loc.latitude
+						})
+					)
 					.then((res) => {
 						if (res.data.code !== 200) {
 							uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
@@ -136,8 +158,12 @@
 						this.candidateList = records.map((item) => this.mapStore(item));
 						this.selectedIds = [];
 					})
-					.catch(() => {
-						uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
+					.catch((err) => {
+						this.candidateList = [];
+						uni.showToast({
+							title: (err && err.message) || '加载失败,请稍后重试',
+							icon: 'none'
+						});
 					})
 					.finally(() => {
 						this.loading = false;

+ 1 - 1
pages/topUp/status.vue

@@ -7,7 +7,7 @@
 				<!-- 审核中 auditStatus=1 -->
 				<template v-if="auditStatus === 1">
 					<view class="title">提交申请</view>
-					<view class="msg">你的提交申请,请耐心等待平台审核。</view>
+					<view class="msg">您已提交申请,请耐心等待平台审核。</view>
 					<view class="btn" @click="goHome">我知道了</view>
 				</template>