Browse Source

Merge branch 'redesign‌-260706' of git.huaml.com:zhh/front-end into redesign‌-260706

shizhongqi 13 giờ trước cách đây
mục cha
commit
5d663f2c7a

+ 2 - 0
hd/src/cool/request/index.js

@@ -7,6 +7,7 @@ import order from '@/service/order/index';
 import refund from '@/service/refund/index';
 import refund from '@/service/refund/index';
 import delivery from '@/service/delivery/index';
 import delivery from '@/service/delivery/index';
 import work from '@/service/work/index';
 import work from '@/service/work/index';
+import psMethod from '@/service/ps-method/index';
 import member from '@/service/member/index';
 import member from '@/service/member/index';
 import goods from '@/service/goods/index';
 import goods from '@/service/goods/index';
 import level from '@/service/level/index';
 import level from '@/service/level/index';
@@ -46,6 +47,7 @@ export function SET_SERVICE({ store }) {
 		refund,
 		refund,
 		delivery,
 		delivery,
 		work,
 		work,
+		psMethod,
 		member,
 		member,
 		goods,
 		goods,
 		level,
 		level,

+ 17 - 5
hd/src/mock/userInfo.js

@@ -230,16 +230,16 @@ let menu = {
             keepAlive: 1
             keepAlive: 1
         },
         },
 */
 */
-		// 订单管理
+		// 订单管理(type=0 作为目录,子菜单:订单列表、配送方式)
 		{
 		{
 			id: '3',
 			id: '3',
 			parentId: null,
 			parentId: null,
 			name: '订单',
 			name: '订单',
-			type: 1,
+			type: 0,
 			icon: 'icondingdanguanli',
 			icon: 'icondingdanguanli',
 			orderNum: 3,
 			orderNum: 3,
-			router: '/order/list',
-			viewPath: 'views/order/list.vue',
+			router: null,
+			viewPath: null,
 			keepAlive: 1
 			keepAlive: 1
 		},
 		},
         // 订单列表
         // 订单列表
@@ -249,11 +249,23 @@ let menu = {
             name: '订单列表',
             name: '订单列表',
             type: 1,
             type: 1,
             icon: 'icondingdanguanli',
             icon: 'icondingdanguanli',
-            orderNum: 3,
+            orderNum: 1,
             router: '/order/list',
             router: '/order/list',
             viewPath: 'views/order/list.vue',
             viewPath: 'views/order/list.vue',
             keepAlive: 1
             keepAlive: 1
 		},
 		},
+		// 配送方式(对齐小程序 admin/home/psMethod)
+		{
+			id: '302',
+			parentId: 3,
+			name: '配送方式',
+			type: 1,
+			icon: 'icondingdanguanli',
+			orderNum: 2,
+			router: '/order/ps-method',
+			viewPath: 'views/order/ps-method.vue',
+			keepAlive: 1
+		},
 		// 商品管理
 		// 商品管理
 		{
 		{
 			id: '4',
 			id: '4',

+ 10 - 0
hd/src/service/delivery/index.js

@@ -6,6 +6,16 @@ import { BaseService, Service } from '@/cool';
 
 
 @Service('delivery')
 @Service('delivery')
 export class DeliveryService extends BaseService {
 export class DeliveryService extends BaseService {
+	/**
+	 * 跑腿平台列表(含授权状态,供配送方式页判断是否已绑定)
+	 */
+	platformList(params) {
+		return this.request({
+			url: '/list',
+			params
+		});
+	}
+
 	/**
 	/**
 	 * 获取发货/收货地址
 	 * 获取发货/收货地址
 	 */
 	 */

+ 42 - 0
hd/src/service/ps-method/index.js

@@ -0,0 +1,42 @@
+/**
+ * 花掌柜 PC 端配送方式配置接口
+ * 对齐 hdApp/src/api/ps-method,读写 xhPsMethod / xhPsExplain / xhPsReduceRule
+ */
+import { BaseService, Service } from '@/cool';
+
+@Service('ps-method')
+export class PsMethodService extends BaseService {
+	/**
+	 * 获取指定 style 的配送配置
+	 * @param {{ style: number }} params 0送货 1自取 2跑腿 3物流 4快递
+	 */
+	getConfig(params) {
+		return this.request({
+			url: '/get-config',
+			params
+		});
+	}
+
+	/**
+	 * 获取各配送方式排序与别名(底部显示顺序预览)
+	 */
+	getSorts() {
+		return this.request({
+			url: '/get-sorts'
+		});
+	}
+
+	/**
+	 * 保存当前 Tab 配送配置
+	 * @param {Object} data 表单整包(含 explains / reduceRules)
+	 */
+	saveConfig(data) {
+		return this.request({
+			url: '/save-config',
+			method: 'POST',
+			data
+		});
+	}
+}
+
+export default new PsMethodService();

+ 805 - 0
hd/src/views/order/ps-method.vue

@@ -0,0 +1,805 @@
+<!--
+  花掌柜 PC 端配送方式设置
+  布局参考设计稿:上方基础信息表单 + 下方 2×2 规则卡片;接口对齐小程序 psMethod
+-->
+<template>
+	<div class="app-list-content ps-method-page" v-loading="loading">
+		<el-tabs class="ps-tabs" v-model="tabIndex" @tab-click="onTabClick">
+			<el-tab-pane
+				v-for="(tab, index) in tabs"
+				:key="tab.value"
+				:label="tab.name"
+				:name="String(index)"
+			></el-tab-pane>
+		</el-tabs>
+
+		<div class="ps-body">
+			<!-- 上方:基础配置(标签左、控件右,整行分隔) -->
+			<div class="base-panel">
+				<div class="base-row">
+					<div class="base-lab">别名</div>
+					<div class="base-val">
+						<el-input v-model="form.name" maxlength="4" size="small" placeholder="最多4个字" style="width: 200px;"></el-input>
+					</div>
+				</div>
+
+				<template v-if="form.style === 2">
+					<div class="base-row">
+						<div class="base-lab">算运费方式</div>
+						<div class="base-val">
+							<button
+								type="button"
+								class="seg-btn"
+								:class="{ active: form.calcType == 0 }"
+								@click="form.calcType = 0"
+							>用跑腿</button>
+							<button
+								type="button"
+								class="seg-btn"
+								:class="{ active: form.calcType == 1 }"
+								@click="form.calcType = 1"
+							>用自定义</button>
+						</div>
+					</div>
+					<div class="base-row">
+						<div class="base-lab">跑腿提供方</div>
+						<div class="base-val">
+							<span class="provider-text">{{ deliveryBindText }}</span>
+							<el-button type="primary" size="mini" plain @click="onManageProvider">管理</el-button>
+						</div>
+					</div>
+				</template>
+
+				<div class="base-row">
+					<div class="base-lab">状态</div>
+					<div class="base-val">
+						<el-radio-group v-model="form.status" @change="onStatusRadio">
+							<el-radio :label="1">启用</el-radio>
+							<el-radio :label="0">禁用</el-radio>
+						</el-radio-group>
+					</div>
+				</div>
+
+				<div class="base-row">
+					<div class="base-lab">顺序</div>
+					<div class="base-val">
+						<el-input v-model="form.sort" type="number" size="small" style="width: 100px;"></el-input>
+					</div>
+				</div>
+
+				<!-- 送货 / 快递门槛 -->
+				<template v-if="form.style === 0 || form.style === 4">
+					<div class="base-row">
+						<div class="base-lab">最低消费金额</div>
+						<div class="base-val">
+							<el-input v-model="form.minAmount" size="small" placeholder="不限制填0" style="width: 140px;"></el-input>
+						</div>
+					</div>
+					<div class="base-row">
+						<div class="base-lab">最低消费数量</div>
+						<div class="base-val">
+							<el-input v-model="form.minNum" type="number" size="small" placeholder="不限制填0" style="width: 140px;"></el-input>
+						</div>
+					</div>
+					<div class="base-row">
+						<div class="base-lab">不满条件</div>
+						<div class="base-val">
+							<button type="button" class="seg-btn" :class="{ active: form.unMeet == 0 }" @click="form.unMeet = 0">收运费</button>
+							<button type="button" class="seg-btn" :class="{ active: form.unMeet == 2 }" @click="form.unMeet = 2">收包装费</button>
+							<button type="button" class="seg-btn" :class="{ active: form.unMeet == 1 }" @click="form.unMeet = 1">不能买</button>
+						</div>
+					</div>
+					<div class="base-row" v-if="form.unMeet != 1">
+						<div class="base-lab">{{ form.unMeet == 2 ? '包装费' : '运费' }}</div>
+						<div class="base-val">
+							<el-input v-model="form.unMeetFee" size="small" style="width: 140px;"></el-input>
+						</div>
+					</div>
+				</template>
+
+				<div class="base-row" v-if="form.style === 3">
+					<div class="base-lab">配送物流</div>
+					<div class="base-val">
+						<span class="provider-text">请在小程序维护</span>
+						<el-button type="primary" size="mini" plain @click="onManageWl">管理</el-button>
+					</div>
+				</div>
+
+				<div class="base-row last">
+					<div class="base-lab">显示顺序</div>
+					<div class="base-val order-preview">{{ sortedMethodsText }}</div>
+				</div>
+			</div>
+
+			<!-- 下方:2×2 规则区(跑腿 Tab);其它 Tab 仅说明占满 -->
+			<div class="rule-grid" v-if="form.style === 2">
+				<!-- 左上:花束运费 -->
+				<div class="rule-card">
+					<div class="rule-title">花束运费</div>
+					<div class="rule-line">
+						<span>花束包配送的</span>
+						<el-input v-model="form.hsFreeKm" size="mini" class="num-input"></el-input>
+						<span>公里内免费配送</span>
+					</div>
+					<div class="rule-line">
+						<span>{{ form.hsFreeKm || 0 }}公里以上 每公里加收</span>
+						<el-input v-model="form.hsPerKmPrice" size="mini" class="num-input"></el-input>
+						<span>元</span>
+					</div>
+					<div class="rule-tip">没有包配送的,按实际跑腿费或自定义计费规则</div>
+				</div>
+
+				<!-- 右上:花材满减 -->
+				<div class="rule-card">
+					<div class="rule-head">
+						<div class="rule-title">花材满减规则</div>
+						<el-button type="primary" size="mini" @click="addReduceRule">添加项</el-button>
+					</div>
+					<div v-if="form.reduceRules && form.reduceRules.length">
+						<div v-for="(rule, index) in form.reduceRules" :key="'r-' + index" class="rule-line">
+							<span>满</span>
+							<el-input
+								v-model="rule.meetNum"
+								size="mini"
+								class="num-input-sm"
+								@focus="clearReduceRuleInputOnFocus(rule, 'meetNum')"
+								@blur="normalizeReduceRuleInput(rule, 'meetNum')"
+							></el-input>
+							<span>扎,</span>
+							<el-input
+								v-model="rule.meetAmount"
+								size="mini"
+								class="num-input"
+								@focus="clearReduceRuleInputOnFocus(rule, 'meetAmount')"
+								@blur="normalizeReduceRuleInput(rule, 'meetAmount')"
+							></el-input>
+							<span>元,</span>
+							<el-input
+								v-model="rule.freeKm"
+								size="mini"
+								class="num-input-sm"
+								@focus="clearReduceRuleInputOnFocus(rule, 'freeKm')"
+								@blur="normalizeReduceRuleInput(rule, 'freeKm')"
+							></el-input>
+							<span>公里内免费配送</span>
+							<a class="link-del" @click="removeReduceRule(index)">删除</a>
+						</div>
+					</div>
+					<div v-else class="empty-tip">暂无满减规则,点击添加项</div>
+					<div class="rule-tip">花材不符合满减规则时,将通过跑腿计算运费;跑腿无法使用时,将使用自定义计费</div>
+				</div>
+
+				<!-- 左下:自定义计费 -->
+				<div class="rule-card">
+					<div class="rule-title">
+						自定义计费
+						<span class="badge" v-if="form.calcType == 1">当前生效</span>
+						<span class="badge gray" v-else>异常兜底</span>
+					</div>
+					<div class="rule-line">
+						<span class="fixed-lab">起步价</span>
+						<el-input v-model="form.startKm" size="mini" class="num-input-sm"></el-input>
+						<span>km内</span>
+						<el-input v-model="form.startPrice" size="mini" class="num-input"></el-input>
+						<span>元</span>
+					</div>
+					<div class="rule-line">
+						<span class="fixed-lab">超出起步</span>
+						<span>每公里加</span>
+						<el-input v-model="form.perKmPrice" size="mini" class="num-input"></el-input>
+						<span>元</span>
+					</div>
+					<div class="rule-line">
+						<span class="fixed-lab">临时涨价</span>
+						<el-input v-model="form.changeRate" size="mini" class="num-input"></el-input>
+						<span>% (降价请填负数)</span>
+					</div>
+				</div>
+
+				<!-- 右下:说明 -->
+				<div class="rule-card">
+					<div class="rule-head">
+						<div class="rule-title">说明</div>
+						<el-button type="primary" size="mini" @click="addExplain">添加</el-button>
+					</div>
+					<div v-if="form.explains && form.explains.length" class="explain-list">
+						<div v-for="(exp, index) in form.explains" :key="'e-' + index" class="explain-item">
+							<el-input
+								type="textarea"
+								v-model="exp.explain"
+								:maxlength="50"
+								:rows="2"
+								placeholder="最多50字"
+								class="explain-textarea"
+								:class="{
+									'text-red': exp.color == 2,
+									'text-blue': exp.color == 3,
+									'text-bold': exp.fontWeight == 2
+								}"
+							></el-input>
+							<div class="explain-actions">
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 1 }" @click="setExplainColor(index, 1)">黑色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 2 }" @click="setExplainColor(index, 2)">红色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 3 }" @click="setExplainColor(index, 3)">蓝色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.fontWeight == 1 }" @click="setExplainWeight(index, 1)">常规</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.fontWeight == 2 }" @click="setExplainWeight(index, 2)">加粗</button>
+								<a class="link-del" @click="removeExplain(index)">删除</a>
+							</div>
+						</div>
+					</div>
+					<div v-else class="empty-tip">暂无说明,点击添加</div>
+				</div>
+			</div>
+
+			<!-- 非跑腿:说明单独一块 -->
+			<div class="rule-grid single" v-else>
+				<div class="rule-card explain-card-half">
+					<div class="rule-head">
+						<div class="rule-title">说明</div>
+						<el-button type="primary" size="mini" @click="addExplain">添加</el-button>
+					</div>
+					<div v-if="form.explains && form.explains.length" class="explain-list">
+						<div v-for="(exp, index) in form.explains" :key="'e-' + index" class="explain-item">
+							<el-input
+								type="textarea"
+								v-model="exp.explain"
+								:maxlength="50"
+								:rows="2"
+								placeholder="最多50字"
+								class="explain-textarea"
+								:class="{
+									'text-red': exp.color == 2,
+									'text-blue': exp.color == 3,
+									'text-bold': exp.fontWeight == 2
+								}"
+							></el-input>
+							<div class="explain-actions">
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 1 }" @click="setExplainColor(index, 1)">黑色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 2 }" @click="setExplainColor(index, 2)">红色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.color == 3 }" @click="setExplainColor(index, 3)">蓝色</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.fontWeight == 1 }" @click="setExplainWeight(index, 1)">常规</button>
+								<button type="button" class="seg-btn mini" :class="{ active: exp.fontWeight == 2 }" @click="setExplainWeight(index, 2)">加粗</button>
+								<a class="link-del" @click="removeExplain(index)">删除</a>
+							</div>
+						</div>
+					</div>
+					<div v-else class="empty-tip">暂无说明,点击添加</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="ps-footer">
+			<!-- 两个按钮都靠左:取消在前,保存紧跟其后(略放大) -->
+			<el-button size="small" @click="onCancel">取消</el-button>
+			<el-button type="primary" class="btn-save" :loading="saving" @click="submitSave">保存设置</el-button>
+		</div>
+	</div>
+</template>
+
+<script>
+/**
+ * 配送方式设置:布局按设计稿 2×2;逻辑对齐小程序 psMethod
+ */
+export default {
+	name: 'ps-method',
+	data() {
+		return {
+			loading: false,
+			saving: false,
+			tabs: [
+				{ name: '送货', value: 0 },
+				{ name: '自取', value: 1 },
+				{ name: '跑腿', value: 2 },
+				{ name: '快递', value: 4 },
+				{ name: '物流', value: 3 }
+			],
+			tabIndex: '0',
+			sortsList: [],
+			deliveryMerchantBound: false,
+			form: this.createEmptyForm(0)
+		};
+	},
+	computed: {
+		sortedMethodsText() {
+			const enabledList = this.sortsList.filter(item => item.status !== 0);
+			if (!enabledList.length) return '暂无启用的配送方式';
+			const sorted = [...enabledList].sort((a, b) => {
+				if (a.sort !== b.sort) return a.sort - b.sort;
+				return a.style - b.style;
+			});
+			return sorted.map(item => item.name).join(' - ');
+		},
+		/** 设计稿跑腿提供方:已绑定显示「已绑定」,未绑定显示「-」 */
+		deliveryBindText() {
+			return this.deliveryMerchantBound ? '已绑定' : '-';
+		}
+	},
+	created() {
+		this.loadConfig(0);
+		this.loadSorts();
+	},
+	methods: {
+		createEmptyForm(style) {
+			return {
+				id: 0,
+				style: style,
+				name: '',
+				status: 1,
+				sort: 0,
+				minAmount: 0,
+				minNum: 0,
+				unMeet: 0,
+				unMeetFee: 0,
+				calcType: 0,
+				hsFreeKm: 0,
+				hsPerKmPrice: 0,
+				startKm: 0,
+				startPrice: 0,
+				perKmPrice: 0,
+				changeRate: 0,
+				explains: [],
+				reduceRules: []
+			};
+		},
+		onTabClick() {
+			const index = Number(this.tabIndex);
+			const style = this.tabs[index].value;
+			this.loadConfig(style);
+			if (style === 2) this.loadDeliveryBindStatus();
+		},
+		loadSorts() {
+			this.$service.psMethod
+				.getSorts()
+				.then(res => {
+					this.sortsList = Array.isArray(res) ? res : (res && res.list) || [];
+				})
+				.catch(() => {});
+		},
+		loadDeliveryBindStatus() {
+			return this.$service.delivery
+				.platformList()
+				.then(res => {
+					const list = (res && res.platformList) || [];
+					this.deliveryMerchantBound = list.some(item => !!item.is_authorized);
+				})
+				.catch(() => {
+					this.deliveryMerchantBound = false;
+				});
+		},
+		onManageProvider() {
+			this.$message.info('请前往小程序「跑腿管理」完成授权绑定');
+		},
+		onManageWl() {
+			this.$message.info('请前往小程序「管理物流」维护承运商');
+		},
+		onCancel() {
+			this.loadConfig(this.form.style);
+			this.$message.success('已重新加载当前配置');
+		},
+		onStatusRadio(val) {
+			const next = Number(val);
+			const prev = next === 1 ? 0 : 1;
+			if (next === 1 && Number(this.form.style) === 2) {
+				if (!this.hasValidCustomConfig()) {
+					this.$message.warning('请先完整填写自定义计费规则后再启用');
+					this.$nextTick(() => {
+						this.form.status = prev;
+					});
+					return;
+				}
+				if (!this.canEnableRunner()) {
+					this.$message.warning('请先绑定跑腿商户或配置完整的自定义计费后再启用');
+					this.$nextTick(() => {
+						this.form.status = prev;
+					});
+				}
+			}
+		},
+		hasValidCustomConfig() {
+			return (
+				(Number(this.form.startKm) || 0) > 0 &&
+				(Number(this.form.startPrice) || 0) > 0 &&
+				(Number(this.form.perKmPrice) || 0) > 0
+			);
+		},
+		canEnableRunner() {
+			return this.deliveryMerchantBound || this.hasValidCustomConfig();
+		},
+		validateRunnerBeforeSave() {
+			if (Number(this.form.style) !== 2) return '';
+			if (!this.hasValidCustomConfig()) {
+				return '请完整填写自定义计费规则(起步公里、起步价、超出每公里价)';
+			}
+			if (Number(this.form.status) === 1 && !this.canEnableRunner()) {
+				return '请先绑定跑腿商户或配置完整的自定义计费后再启用';
+			}
+			return '';
+		},
+		loadConfig(style) {
+			this.loading = true;
+			this.$service.psMethod
+				.getConfig({ style })
+				.then(data => {
+					const res = data || {};
+					this.form = {
+						id: res.id || 0,
+						style: style,
+						name: res.name || '',
+						status: res.status != null ? Number(res.status) : 1,
+						sort: res.sort != null ? Number(res.sort) : 0,
+						minAmount: res.minAmount != null ? parseFloat(res.minAmount) : 0,
+						minNum: res.minNum != null ? Number(res.minNum) : 0,
+						unMeet: res.unMeet != null ? Number(res.unMeet) : 0,
+						unMeetFee: res.unMeetFee != null ? parseFloat(res.unMeetFee) : 0,
+						calcType: res.calcType != null ? Number(res.calcType) : 0,
+						hsFreeKm: res.hsFreeKm != null ? parseFloat(res.hsFreeKm) : 0,
+						hsPerKmPrice: res.hsPerKmPrice != null ? parseFloat(res.hsPerKmPrice) : 0,
+						startKm: res.startKm != null ? parseFloat(res.startKm) : 0,
+						startPrice: res.startPrice != null ? parseFloat(res.startPrice) : 0,
+						perKmPrice: res.perKmPrice != null ? parseFloat(res.perKmPrice) : 0,
+						changeRate: res.changeRate != null ? parseFloat(res.changeRate) : 0,
+						explains: (res.explains || []).map(exp => ({
+							...exp,
+							color: exp.color != null ? Number(exp.color) : 1,
+							fontWeight: exp.fontWeight != null ? Number(exp.fontWeight) : 1
+						})),
+						reduceRules: (res.reduceRules || []).map(rule => ({
+							...rule,
+							meetNum: rule.meetNum != null ? Number(rule.meetNum) : 0,
+							meetAmount:
+								rule.meetAmount != null && rule.meetAmount !== ''
+									? parseFloat(Number(rule.meetAmount))
+									: 0,
+							freeKm:
+								rule.freeKm != null && rule.freeKm !== ''
+									? parseFloat(Number(rule.freeKm))
+									: 0,
+							sort: rule.sort != null ? Number(rule.sort) : 0
+						}))
+					};
+					if (style === 2) this.loadDeliveryBindStatus();
+				})
+				.catch(() => {
+					this.$message.error('加载失败,请重试');
+				})
+				.finally(() => {
+					this.loading = false;
+				});
+		},
+		addReduceRule() {
+			this.form.reduceRules.push({
+				meetNum: 0,
+				meetAmount: 0,
+				freeKm: 0,
+				sort: this.form.reduceRules.length
+			});
+		},
+		clearReduceRuleInputOnFocus(rule, field) {
+			this.$set(rule, field, '');
+		},
+		normalizeReduceRuleInput(rule, field) {
+			const value = rule[field];
+			if (value === '' || value == null) {
+				this.$set(rule, field, 0);
+				return;
+			}
+			const num = Number(value);
+			if (Number.isNaN(num)) {
+				this.$set(rule, field, 0);
+				return;
+			}
+			this.$set(rule, field, field === 'meetNum' ? parseInt(num, 10) : parseFloat(String(num)));
+		},
+		removeReduceRule(index) {
+			this.$confirm('确认删除该满减规则吗?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			})
+				.then(() => this.form.reduceRules.splice(index, 1))
+				.catch(() => {});
+		},
+		addExplain() {
+			if (this.form.explains.length >= 5) {
+				this.$message.warning('最多只能添加5条说明');
+				return;
+			}
+			this.form.explains.push({
+				explain: '',
+				color: 1,
+				fontWeight: 1,
+				sort: this.form.explains.length
+			});
+		},
+		removeExplain(index) {
+			this.$confirm('确认删除该说明吗?', '提示', {
+				confirmButtonText: '确定',
+				cancelButtonText: '取消',
+				type: 'warning'
+			})
+				.then(() => this.form.explains.splice(index, 1))
+				.catch(() => {});
+		},
+		setExplainColor(index, color) {
+			const exp = this.form.explains[index];
+			if (!exp) return;
+			this.$set(this.form.explains, index, { ...exp, color });
+		},
+		setExplainWeight(index, fontWeight) {
+			const exp = this.form.explains[index];
+			if (!exp) return;
+			this.$set(this.form.explains, index, { ...exp, fontWeight });
+		},
+		submitSave() {
+			if (!this.form.name) {
+				this.$message.warning('请填写别名');
+				return;
+			}
+			if (this.form.name.length > 4) {
+				this.$message.warning('名称最多4个字');
+				return;
+			}
+			for (let i = 0; i < this.form.explains.length; i++) {
+				if (!this.form.explains[i].explain) {
+					this.$message.warning(`请填写第 ${i + 1} 条说明内容`);
+					return;
+				}
+			}
+			const runnerErr = this.validateRunnerBeforeSave();
+			if (runnerErr) {
+				this.$message.warning(runnerErr);
+				return;
+			}
+			this.saving = true;
+			this.$service.psMethod
+				.saveConfig(this.form)
+				.then(() => {
+					this.$message.success('保存成功');
+					this.loadConfig(this.form.style);
+					this.loadSorts();
+				})
+				.finally(() => {
+					this.saving = false;
+				});
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.ps-method-page {
+	display: flex;
+	flex-direction: column;
+	height: calc(100vh - 100px);
+	background: #f0f2f5;
+	overflow: hidden;
+}
+.ps-tabs {
+	background: #fff;
+	padding: 0 16px;
+	flex-shrink: 0;
+	::v-deep .el-tabs__header {
+		margin: 0;
+	}
+	::v-deep .el-tabs__item {
+		height: 44px;
+		line-height: 44px;
+	}
+	::v-deep .el-tabs__nav-wrap::after {
+		height: 1px;
+	}
+}
+.ps-body {
+	flex: 1;
+	overflow: auto;
+	padding: 12px 16px;
+}
+.base-panel {
+	background: #fff;
+	border-radius: 4px;
+	border: 1px solid #e8e8e8;
+	margin-bottom: 12px;
+	overflow: hidden;
+}
+.base-row {
+	display: flex;
+	align-items: center;
+	min-height: 38px;
+	padding: 4px 20px;
+	border-bottom: 1px solid #f0f0f0;
+	&.last {
+		border-bottom: none;
+	}
+}
+.base-lab {
+	width: 110px;
+	flex-shrink: 0;
+	color: #595959;
+	font-size: 13px;
+}
+.base-val {
+	flex: 1;
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+}
+.order-preview {
+	color: #262626;
+	font-size: 13px;
+	font-weight: 500;
+}
+.provider-text {
+	margin-right: 12px;
+	color: #595959;
+	min-width: 20px;
+}
+
+/* 分段按钮:选中蓝底白字 */
+.seg-btn {
+	display: inline-block;
+	height: 28px;
+	line-height: 26px;
+	padding: 0 14px;
+	margin-right: 8px;
+	font-size: 12px;
+	color: #595959;
+	background: #fff;
+	border: 1px solid #d9d9d9;
+	border-radius: 3px;
+	cursor: pointer;
+	outline: none;
+	&.active {
+		color: #fff;
+		background: #1890ff;
+		border-color: #1890ff;
+	}
+	&.mini {
+		height: 24px;
+		line-height: 22px;
+		padding: 0 10px;
+		margin-right: 6px;
+	}
+}
+
+.rule-grid {
+	display: grid;
+	grid-template-columns: 1fr 1fr;
+	grid-gap: 12px;
+	&.single {
+		grid-template-columns: 1fr;
+	}
+}
+.rule-card {
+	background: #fff;
+	border: 1px solid #e8e8e8;
+	border-radius: 4px;
+	padding: 14px 16px;
+	min-height: 160px;
+	box-sizing: border-box;
+}
+.rule-head {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 10px;
+}
+.rule-title {
+	font-size: 14px;
+	font-weight: 600;
+	color: #262626;
+	margin-bottom: 10px;
+}
+.rule-head .rule-title {
+	margin-bottom: 0;
+}
+.badge {
+	display: inline-block;
+	margin-left: 8px;
+	padding: 0 6px;
+	font-size: 11px;
+	font-weight: 400;
+	color: #52c41a;
+	background: #f6ffed;
+	border: 1px solid #b7eb8f;
+	border-radius: 2px;
+	&.gray {
+		color: #8c8c8c;
+		background: #fafafa;
+		border-color: #d9d9d9;
+	}
+}
+.rule-line {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+	font-size: 13px;
+	color: #595959;
+	margin-bottom: 10px;
+	line-height: 28px;
+}
+.fixed-lab {
+	width: 64px;
+	flex-shrink: 0;
+	color: #8c8c8c;
+}
+.num-input {
+	width: 72px !important;
+	margin: 0 6px;
+}
+.num-input-sm {
+	width: 56px !important;
+	margin: 0 6px;
+}
+.rule-tip {
+	font-size: 12px;
+	color: #52c41a;
+	line-height: 1.5;
+	margin-top: 4px;
+}
+.empty-tip {
+	font-size: 12px;
+	color: #bfbfbf;
+	padding: 8px 0;
+}
+.explain-card-half {
+	/* 非跑腿时说明板块整体半屏 */
+	max-width: 50%;
+}
+.explain-item {
+	margin-bottom: 10px;
+}
+/* 说明输入框宽度约半屏,卡片内不超过父级 */
+.explain-textarea {
+	display: block;
+	width: 50vw !important;
+	max-width: 100%;
+}
+.explain-textarea ::v-deep .el-textarea__inner {
+	width: 100%;
+	box-sizing: border-box;
+}
+.explain-actions {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+	margin-top: 8px;
+	width: 50vw;
+	max-width: 100%;
+}
+.link-del {
+	margin-left: 10px;
+	color: #1890ff;
+	cursor: pointer;
+	font-size: 12px;
+	&:hover {
+		opacity: 0.8;
+	}
+}
+.text-red {
+	color: #f5222d;
+}
+.text-blue {
+	color: #1890ff;
+}
+.text-bold {
+	font-weight: 700;
+}
+.ps-footer {
+	flex-shrink: 0;
+	display: flex;
+	justify-content: flex-start;
+	align-items: center;
+	padding: 12px 16px;
+	background: #fff;
+	border-top: 1px solid #e8e8e8;
+	.el-button + .el-button {
+		margin-left: 10px;
+	}
+	.btn-save {
+		min-width: 110px;
+		height: 36px;
+		padding: 0 22px;
+		font-size: 14px;
+	}
+}
+</style>

+ 30 - 1
mallApp/src/pages/home/shop-category.vue

@@ -1346,14 +1346,22 @@ export default {
 }
 }
 
 
 .content-panel {
 .content-panel {
+  /* width:0 + min-width:0:避免花材列表把右侧栏撑宽,从而连带撑大轮播 */
   flex: 1;
   flex: 1;
+  width: 0;
+  min-width: 0;
   height: 100%;
   height: 100%;
   background: #fff;
   background: #fff;
+  overflow: hidden;
+  box-sizing: border-box;
 }
 }
 
 
 .ad-section {
 .ad-section {
   position: relative;
   position: relative;
-  margin: 16upx 16upx 0;
+  /* 右侧 24upx 与公告 notice-bar 对齐;宽度锁定在内容栏内 */
+  margin: 16upx 24upx 0 16upx;
+  box-sizing: border-box;
+  width: auto;
   border-radius: 16upx;
   border-radius: 16upx;
   overflow: hidden;
   overflow: hidden;
 }
 }
@@ -1363,7 +1371,11 @@ export default {
 }
 }
 
 
 .ad-swiper {
 .ad-swiper {
+  display: block;
+  width: 100%;
   height: 200upx;
   height: 200upx;
+  border-radius: 16upx;
+  overflow: hidden;
 }
 }
 
 
 .ad-slide {
 .ad-slide {
@@ -1491,10 +1503,27 @@ export default {
 
 
 .item-product-list {
 .item-product-list {
   padding: 0 16upx;
   padding: 0 16upx;
+  width: 100%;
+  box-sizing: border-box;
+  overflow: hidden;
 }
 }
 
 
 .item-product-row {
 .item-product-row {
   position: relative;
   position: relative;
+  width: 100%;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+/* 花材组件标题在 item 页写死 380upx,窄栏下会撑破布局;此处改为随容器收缩 */
+.item-product-list ::v-deep .product-item {
+  max-width: 100%;
+  overflow: hidden;
+}
+
+.item-product-list ::v-deep .item-cmd_bx .cmd-info_bx .tit {
+  width: auto !important;
+  max-width: 100%;
 }
 }
 
 
 /* 整页全宽 FooterCart:bottom 须高于 Tab 栏(115upx + 安全区),避免真机重叠 */
 /* 整页全宽 FooterCart:bottom 须高于 Tab 栏(115upx + 安全区),避免真机重叠 */