settings.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231
  1. <template>
  2. <view class="page">
  3. <u-navbar title="储值设置" :autoBack="true" :placeholder="true"></u-navbar>
  4. <!-- 是否接受共享 -->
  5. <view class="card switch-card">
  6. <text class="label">是否接受其他门店共享储值</text>
  7. <u-switch v-model="form.acceptShare" activeColor="#1C7CF9"></u-switch>
  8. </view>
  9. <!-- 自定义最低金额 -->
  10. <view class="card min-card">
  11. <view class="min-head">
  12. <text class="label">自定义充值最低金额</text>
  13. <text class="min-tip">默认为固定档位中的最低金额</text>
  14. </view>
  15. <view class="min-input-wrap">
  16. <text class="currency">¥</text>
  17. <input
  18. class="min-input"
  19. type="number"
  20. v-model="form.minAmount"
  21. :placeholder="String(lowestTierAmount || '')"
  22. placeholder-class="placeholder"
  23. @input="onMinAmountInput"
  24. />
  25. </view>
  26. </view>
  27. <!-- 固定充值 -->
  28. <view class="section-head">
  29. <view class="section-title-row">
  30. <view class="section-bar"></view>
  31. <text class="section-title">固定充值</text>
  32. </view>
  33. <text class="section-tip">*可点击加号添加固定充值档位</text>
  34. </view>
  35. <view class="tier-list">
  36. <view class="tier-card" v-for="(item, index) in form.tiers" :key="item.id || index">
  37. <view class="tier-top">
  38. <text class="tier-name">档位{{ index + 1 }}</text>
  39. <view class="tier-ops">
  40. <view class="op" @click.stop="openTierEdit(index)">
  41. <u-icon name="edit-pen" color="#1C7CF9" size="35"></u-icon>
  42. </view>
  43. <view class="op" @click.stop="removeTier(index)">
  44. <u-icon name="trash" color="#1C7CF9" size="35"></u-icon>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="tier-body" @click="openTierEdit(index)">
  49. <view class="tier-line">
  50. <text class="k">充值金额</text>
  51. <text class="v">{{ item.amount }}</text>
  52. </view>
  53. <view class="tier-line">
  54. <text class="k">赠送金额</text>
  55. <text class="v">{{ item.giftAmount || 0 }}</text>
  56. </view>
  57. <view class="tier-line">
  58. <text class="k">优惠券</text>
  59. <text class="v">{{ getCouponCount(item) }}张</text>
  60. </view>
  61. </view>
  62. </view>
  63. <view class="empty" v-if="!form.tiers.length && !loading">暂无固定充值档位,请点击加号添加</view>
  64. </view>
  65. <!-- 添加档位 -->
  66. <image class="fab" src="/static/topUp/add.png" mode="aspectFit" @click="openTierAdd"></image>
  67. <!-- 底部保存 -->
  68. <view class="footer">
  69. <view class="save-btn" :class="{ disabled: saving }" @click="onSave">
  70. {{ saving ? '保存中...' : '保存' }}
  71. </view>
  72. </view>
  73. <!-- 固定充值编辑弹层 -->
  74. <u-popup v-model="tierVisible" mode="right" width="100%" :mask-close-able="false">
  75. <view class="sub-page tier-page">
  76. <view class="sub-nav">
  77. <view class="sub-nav-back" @click="closeTier">
  78. <u-icon name="arrow-left" color="#333" size="36"></u-icon>
  79. </view>
  80. <text class="sub-nav-title">储值设置</text>
  81. </view>
  82. <view class="form-card">
  83. <view class="form-title">{{ tierFormTitle }}</view>
  84. <view class="form-row">
  85. <text class="form-label">充值金额</text>
  86. <input
  87. class="form-input"
  88. type="number"
  89. v-model="tierForm.amount"
  90. placeholder="请输入正整数"
  91. placeholder-class="form-placeholder"
  92. @input="onTierAmountInput('amount', $event)"
  93. />
  94. </view>
  95. <view class="form-row">
  96. <text class="form-label">赠送金额</text>
  97. <input
  98. class="form-input"
  99. type="number"
  100. v-model="tierForm.giftAmount"
  101. placeholder="选填,默认0"
  102. placeholder-class="form-placeholder"
  103. @input="onTierAmountInput('giftAmount', $event)"
  104. />
  105. </view>
  106. <view class="form-row link-row" @click="openCouponSelect">
  107. <text class="form-label">赠送优惠券</text>
  108. <view class="link-right">
  109. <text class="link-text" :class="{ active: couponSelectedCount > 0 }">
  110. {{ couponSelectedCount > 0 ? `已选 ${couponSelectedCount} 张` : '去选择' }}
  111. </text>
  112. <u-icon name="arrow-right" color="#c0c4cc" size="14"></u-icon>
  113. </view>
  114. </view>
  115. </view>
  116. <view class="footer tier-footer">
  117. <view class="save-btn" :class="{ disabled: !canSubmitTier }" @click="submitTier">
  118. {{ tierEditIndex === -1 ? '确认添加' : '确认修改' }}
  119. </view>
  120. </view>
  121. </view>
  122. </u-popup>
  123. <!-- 优惠券选择弹层 -->
  124. <u-popup v-model="couponVisible" mode="right" width="100%" :mask-close-able="false">
  125. <view class="sub-page coupon-page">
  126. <view class="sub-nav">
  127. <view class="sub-nav-back" @click="closeCouponSelect">
  128. <u-icon name="arrow-left" color="#333" size="36"></u-icon>
  129. </view>
  130. <text class="sub-nav-title">优惠券</text>
  131. </view>
  132. <view class="tabs">
  133. <view
  134. class="tab"
  135. v-for="(tab, idx) in couponTabs"
  136. :key="idx"
  137. :class="{ active: couponTab === idx }"
  138. @click="switchCouponTab(idx)"
  139. >
  140. <text class="tab-text">{{ tab }}</text>
  141. <image
  142. v-if="couponTab === idx"
  143. class="tab-indicator"
  144. src="/static/topUp/tabs.png"
  145. mode="aspectFit"
  146. ></image>
  147. </view>
  148. </view>
  149. <scroll-view scroll-y class="coupon-scroll">
  150. <view
  151. class="coupon-item"
  152. v-for="item in currentCouponList"
  153. :key="item.id"
  154. @click="toggleCoupon(item)"
  155. >
  156. <view class="radio" :class="{ checked: isCouponChecked(item.id) }">
  157. <u-icon v-if="isCouponChecked(item.id)" name="checkmark" color="#fff" size="12"></u-icon>
  158. </view>
  159. <view class="coupon-ticket">
  160. <image
  161. class="ticket-bg"
  162. src="/static/topUp/bg-coupon-border.png"
  163. mode="scaleToFill"
  164. ></image>
  165. <view class="ticket-left">
  166. <text class="ticket-value" :class="{ multi: couponTab === 1 }">{{ item.display }}</text>
  167. </view>
  168. <view class="ticket-right">
  169. <text class="cname">{{ item.name }}</text>
  170. <text class="ccond" v-if="item.condition">{{ item.condition }}</text>
  171. <view class="ticket-bottom">
  172. <text class="cvalid">{{ item.validText }}</text>
  173. <view class="stepper" @click.stop>
  174. <view
  175. class="step-btn minus"
  176. :class="{ active: getCouponQty(item.id) > 0 }"
  177. @click="changeCouponQty(item.id, -1)"
  178. >
  179. <text class="step-icon">−</text>
  180. </view>
  181. <text class="step-num">{{ getCouponQty(item.id) }}</text>
  182. <view class="step-btn plus" @click="changeCouponQty(item.id, 1)">
  183. <text class="step-icon">+</text>
  184. </view>
  185. </view>
  186. </view>
  187. </view>
  188. </view>
  189. </view>
  190. <view class="empty" v-if="!currentCouponList.length && !couponLoading">暂无优惠券</view>
  191. <view class="empty" v-if="couponLoading">加载中...</view>
  192. </scroll-view>
  193. <view class="footer coupon-footer">
  194. <view class="save-btn" @click="confirmCouponSelect">
  195. {{ tempCouponCount > 0 ? `确定选择(已选 ${tempCouponCount} 张)` : '确定选择' }}
  196. </view>
  197. </view>
  198. </view>
  199. </u-popup>
  200. </view>
  201. </template>
  202. <script>
  203. import prepaidApi from '@/api/memberPrepaid.js'
  204. function createId() {
  205. return `t_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
  206. }
  207. /** 默认固定档位:档位1金额100、档位2金额200,赠送与优惠券均为0 */
  208. function createDefaultTiers() {
  209. return [
  210. { id: createId(), amount: 100, giftAmount: 0, coupons: [] },
  211. { id: createId(), amount: 200, giftAmount: 0, coupons: [] }
  212. ];
  213. }
  214. function formatValidText(beginTime, endTime) {
  215. if (!beginTime && !endTime) return '长期有效';
  216. const begin = beginTime ? String(beginTime).slice(0, 10) : '';
  217. const end = endTime ? String(endTime).slice(0, 10) : '';
  218. if (begin && end) return `${begin} 至 ${end}`;
  219. return begin || end || '长期有效';
  220. }
  221. function isPlainAmount(val) {
  222. return /^[\d.]+$/.test(String(val || '').trim());
  223. }
  224. function pickAmount(val) {
  225. const matched = String(val || '').match(/[\d.]+/);
  226. return matched ? matched[0] : '';
  227. }
  228. /** 折扣券 / 满减券左侧面额与使用条件文案(接口多为纯数字,需补「元」「折」) */
  229. function formatCouponTexts(item) {
  230. const isDiscount = String(item.couponType) === '2';
  231. const info = String(item.contentInfo || '').trim();
  232. const cond = String(item.contentConditions || '').trim();
  233. const infoNum = isPlainAmount(info) ? info : pickAmount(info);
  234. const condNum = isPlainAmount(cond) ? cond : pickAmount(cond);
  235. const noThreshold = !condNum || condNum === '0';
  236. if (isDiscount) {
  237. const display = infoNum ? `${infoNum}折` : info || '折扣';
  238. const condition = noThreshold
  239. ? '不限制条件使用'
  240. : `满${condNum}元可使用`;
  241. return { display, condition };
  242. }
  243. // 满减:左侧「满X元 / 减Y元」
  244. let display = '';
  245. if (infoNum && condNum) {
  246. display = `满${condNum}元\n减${infoNum}元`;
  247. } else if (infoNum) {
  248. display = `${infoNum}元`;
  249. } else if (info) {
  250. display = info
  251. .replace(/满\s*([\d.]+)\s*元?/g, '满$1元')
  252. .replace(/减\s*([\d.]+)\s*元?/g, '减$1元');
  253. } else {
  254. display = '优惠';
  255. }
  256. const condition = noThreshold
  257. ? '不限制条件使用'
  258. : `满${condNum}元可抵扣`;
  259. return { display, condition };
  260. }
  261. export default {
  262. data() {
  263. return {
  264. loading: false,
  265. saving: false,
  266. configVersion: null,
  267. form: {
  268. acceptShare: true,
  269. minAmount: '100',
  270. tiers: createDefaultTiers()
  271. },
  272. tierVisible: false,
  273. tierEditIndex: -1,
  274. tierForm: {
  275. amount: '',
  276. giftAmount: '',
  277. coupons: []
  278. },
  279. couponVisible: false,
  280. couponTab: 0,
  281. couponTabs: ['折扣券', '满减券'],
  282. tempCoupons: [],
  283. discountCoupons: [],
  284. reduceCoupons: [],
  285. couponLoading: false
  286. };
  287. },
  288. computed: {
  289. hasGiftConfig() {
  290. return (this.form.tiers || []).some((tier) => {
  291. const gift = Number(tier.giftAmount) || 0;
  292. const couponQty = (tier.coupons || []).reduce((sum, c) => sum + (Number(c.qty) || 0), 0);
  293. return gift > 0 || couponQty > 0;
  294. });
  295. },
  296. /** 固定档位中的最低充值金额,用作自定义最低金额默认值 */
  297. lowestTierAmount() {
  298. const amounts = (this.form.tiers || [])
  299. .map((t) => Number(t.amount))
  300. .filter((n) => !Number.isNaN(n) && n > 0);
  301. if (!amounts.length) return 0;
  302. return Math.min(...amounts);
  303. },
  304. canSubmitTier() {
  305. const amount = String(this.tierForm.amount || '').trim();
  306. return !!amount && /^\d+$/.test(amount) && Number(amount) > 0;
  307. },
  308. tierFormTitle() {
  309. if (this.tierEditIndex === -1) {
  310. return `档位${(this.form.tiers || []).length + 1}`;
  311. }
  312. return `档位${this.tierEditIndex + 1}`;
  313. },
  314. couponSelectedCount() {
  315. return (this.tierForm.coupons || []).reduce((sum, c) => sum + (Number(c.qty) || 0), 0);
  316. },
  317. tempCouponCount() {
  318. return (this.tempCoupons || []).reduce((sum, c) => sum + (Number(c.qty) || 0), 0);
  319. },
  320. currentCouponList() {
  321. return this.couponTab === 0 ? this.discountCoupons : this.reduceCoupons;
  322. }
  323. },
  324. onLoad() {
  325. this.loadSettings();
  326. },
  327. methods: {
  328. onMinAmountInput(e) {
  329. const val = String((e && e.detail && e.detail.value) || this.form.minAmount || '')
  330. .replace(/[^\d]/g, '');
  331. this.$nextTick(() => {
  332. this.form.minAmount = val;
  333. });
  334. },
  335. onTierAmountInput(field, e) {
  336. const val = String((e && e.detail && e.detail.value) || this.tierForm[field] || '')
  337. .replace(/[^\d]/g, '');
  338. this.$nextTick(() => {
  339. this.$set(this.tierForm, field, val);
  340. });
  341. },
  342. loadSettings() {
  343. this.loading = true;
  344. prepaidApi
  345. .getRechargeConfig()
  346. .then((res) => {
  347. if (res.data.code !== 200 || !res.data.result) {
  348. uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
  349. return;
  350. }
  351. const data = res.data.result;
  352. this.configVersion = data.version;
  353. const tiers = Array.isArray(data.tiers) ? data.tiers : [];
  354. const mappedTiers = tiers.length
  355. ? tiers.map((tier) => ({
  356. id: tier.tierId || createId(),
  357. amount: Number(tier.rechargeAmount) || 0,
  358. giftAmount: Number(tier.giftAmount) || 0,
  359. coupons: (tier.coupons || []).map((c) => ({
  360. id: c.couponId,
  361. name: c.couponName || '',
  362. type: String(c.couponType) === '2' ? 'discount' : 'reduce',
  363. qty: Number(c.quantity) || 1
  364. }))
  365. }))
  366. : createDefaultTiers();
  367. const lowest = mappedTiers
  368. .map((t) => Number(t.amount))
  369. .filter((n) => n > 0);
  370. const defaultMin = lowest.length ? Math.min(...lowest) : 100;
  371. this.form = {
  372. acceptShare: data.acceptSharedPrepaid !== false,
  373. minAmount:
  374. data.customMinAmount != null && data.customMinAmount !== ''
  375. ? String(Math.floor(Number(data.customMinAmount)) || '')
  376. : String(defaultMin),
  377. tiers: mappedTiers
  378. };
  379. })
  380. .catch(() => {
  381. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  382. })
  383. .finally(() => {
  384. this.loading = false;
  385. });
  386. },
  387. getCouponCount(item) {
  388. return (item.coupons || []).reduce((sum, c) => sum + (Number(c.qty) || 0), 0);
  389. },
  390. openTierAdd() {
  391. this.tierEditIndex = -1;
  392. this.tierForm = { amount: '', giftAmount: '', coupons: [] };
  393. this.tierVisible = true;
  394. },
  395. openTierEdit(index) {
  396. const item = this.form.tiers[index];
  397. this.tierEditIndex = index;
  398. this.tierForm = {
  399. amount: item.amount != null ? String(Math.floor(Number(item.amount)) || '') : '',
  400. giftAmount:
  401. item.giftAmount != null ? String(Math.floor(Number(item.giftAmount)) || 0) : '',
  402. coupons: JSON.parse(JSON.stringify(item.coupons || []))
  403. };
  404. this.tierVisible = true;
  405. },
  406. closeTier() {
  407. this.tierVisible = false;
  408. },
  409. removeTier(index) {
  410. if ((this.form.tiers || []).length <= 2) {
  411. uni.showToast({ title: '至少保留2个固定充值档位', icon: 'none' });
  412. return;
  413. }
  414. uni.showModal({
  415. title: '提示',
  416. content: '确认删除该充值档位吗?',
  417. confirmColor: '#fa3534',
  418. success: (res) => {
  419. if (res.confirm) {
  420. this.form.tiers.splice(index, 1);
  421. }
  422. }
  423. });
  424. },
  425. submitTier() {
  426. if (!this.canSubmitTier) return;
  427. const amountStr = String(this.tierForm.amount || '').trim();
  428. if (!/^\d+$/.test(amountStr) || Number(amountStr) <= 0) {
  429. uni.showToast({ title: '充值金额须为正整数', icon: 'none' });
  430. return;
  431. }
  432. const amount = Number(amountStr);
  433. const giftRaw = String(this.tierForm.giftAmount || '').trim();
  434. if (giftRaw !== '' && !/^\d+$/.test(giftRaw)) {
  435. uni.showToast({ title: '赠送金额须为非负整数', icon: 'none' });
  436. return;
  437. }
  438. const giftAmount = giftRaw === '' ? 0 : Number(giftRaw);
  439. const payload = {
  440. id: this.tierEditIndex === -1 ? createId() : this.form.tiers[this.tierEditIndex].id,
  441. amount,
  442. giftAmount,
  443. coupons: JSON.parse(JSON.stringify(this.tierForm.coupons || []))
  444. };
  445. if (this.tierEditIndex === -1) {
  446. this.form.tiers.push(payload);
  447. } else {
  448. this.$set(this.form.tiers, this.tierEditIndex, payload);
  449. }
  450. this.tierVisible = false;
  451. },
  452. mapCouponItem(item) {
  453. const { display, condition } = formatCouponTexts(item);
  454. return {
  455. id: item.couponId,
  456. name: item.couponName || '',
  457. display,
  458. condition,
  459. validText: formatValidText(item.beginTime, item.endTime),
  460. remainingQuantity: Number(item.remainingQuantity) || 0
  461. };
  462. },
  463. switchCouponTab(idx) {
  464. this.couponTab = idx;
  465. this.loadCoupons();
  466. },
  467. openCouponSelect() {
  468. this.tempCoupons = JSON.parse(JSON.stringify(this.tierForm.coupons || []));
  469. this.couponTab = 0;
  470. this.couponVisible = true;
  471. this.loadCoupons();
  472. },
  473. closeCouponSelect() {
  474. this.couponVisible = false;
  475. },
  476. loadCoupons() {
  477. this.couponLoading = true;
  478. // couponType: 1满减 2折扣;tabs: 0折扣 1满减
  479. const couponType = this.couponTab === 0 ? '2' : '1';
  480. prepaidApi
  481. .getCouponPage({ couponType, pageNo: 1, pageSize: 100 })
  482. .then((res) => {
  483. if (res.data.code !== 200) {
  484. uni.showToast({ title: res.data.message || '优惠券加载失败', icon: 'none' });
  485. return;
  486. }
  487. const page = res.data.result || {};
  488. const records = Array.isArray(page.records) ? page.records : [];
  489. const list = records.map((item) => this.mapCouponItem(item));
  490. if (this.couponTab === 0) {
  491. this.discountCoupons = list;
  492. } else {
  493. this.reduceCoupons = list;
  494. }
  495. })
  496. .catch(() => {
  497. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  498. })
  499. .finally(() => {
  500. this.couponLoading = false;
  501. });
  502. },
  503. isCouponChecked(id) {
  504. return this.tempCoupons.some((c) => c.id === id && Number(c.qty) > 0);
  505. },
  506. getCouponQty(id) {
  507. const found = this.tempCoupons.find((c) => c.id === id);
  508. return found ? Number(found.qty) || 0 : 0;
  509. },
  510. findCouponMeta(id) {
  511. return (
  512. this.discountCoupons.find((c) => c.id === id) ||
  513. this.reduceCoupons.find((c) => c.id === id) ||
  514. null
  515. );
  516. },
  517. toggleCoupon(item) {
  518. const idx = this.tempCoupons.findIndex((c) => c.id === item.id);
  519. if (idx >= 0 && Number(this.tempCoupons[idx].qty) > 0) {
  520. this.tempCoupons.splice(idx, 1);
  521. return;
  522. }
  523. if (idx >= 0) {
  524. this.$set(this.tempCoupons[idx], 'qty', 1);
  525. } else {
  526. this.tempCoupons.push({
  527. id: item.id,
  528. name: item.name,
  529. type: this.couponTab === 0 ? 'discount' : 'reduce',
  530. qty: 1
  531. });
  532. }
  533. },
  534. changeCouponQty(id, delta) {
  535. const idx = this.tempCoupons.findIndex((c) => c.id === id);
  536. const meta = this.findCouponMeta(id);
  537. if (idx < 0) {
  538. if (delta > 0 && meta) {
  539. this.tempCoupons.push({
  540. id,
  541. name: meta.name,
  542. type: this.couponTab === 0 ? 'discount' : 'reduce',
  543. qty: 1
  544. });
  545. }
  546. return;
  547. }
  548. const next = Math.max(0, (Number(this.tempCoupons[idx].qty) || 0) + delta);
  549. if (next === 0) {
  550. this.tempCoupons.splice(idx, 1);
  551. } else {
  552. this.$set(this.tempCoupons[idx], 'qty', next);
  553. }
  554. },
  555. confirmCouponSelect() {
  556. this.tierForm.coupons = this.tempCoupons
  557. .filter((c) => Number(c.qty) > 0)
  558. .map((c) => ({ ...c, qty: Number(c.qty) }));
  559. this.couponVisible = false;
  560. },
  561. onSave() {
  562. if (this.saving) return;
  563. if (this.form.tiers.length < 2) {
  564. uni.showToast({ title: '至少配置2个固定充值档位', icon: 'none' });
  565. return;
  566. }
  567. const amounts = this.form.tiers.map((t) => Number(t.amount) || 0);
  568. for (let i = 0; i < amounts.length; i++) {
  569. if (!Number.isInteger(amounts[i]) || amounts[i] <= 0) {
  570. uni.showToast({ title: '档位充值金额须为正整数', icon: 'none' });
  571. return;
  572. }
  573. }
  574. for (let i = 1; i < amounts.length; i++) {
  575. if (amounts[i] <= amounts[i - 1]) {
  576. uni.showToast({ title: '档位充值金额须严格递增', icon: 'none' });
  577. return;
  578. }
  579. }
  580. // 有赠送余额/优惠券时接口要求关闭自定义充值
  581. const customAmountEnabled = !this.hasGiftConfig;
  582. const minRaw = String(this.form.minAmount || '').trim();
  583. const lowest = this.lowestTierAmount;
  584. let customMinAmount = lowest || 0;
  585. if (minRaw !== '') {
  586. if (!/^\d+$/.test(minRaw) || Number(minRaw) <= 0) {
  587. uni.showToast({ title: '自定义最低金额须为正整数', icon: 'none' });
  588. return;
  589. }
  590. customMinAmount = Number(minRaw);
  591. if (lowest && customMinAmount < lowest) {
  592. uni.showToast({
  593. title: `自定义最低金额不能低于最低档位(${lowest})`,
  594. icon: 'none'
  595. });
  596. return;
  597. }
  598. } else if (!lowest) {
  599. uni.showToast({ title: '请配置固定充值档位', icon: 'none' });
  600. return;
  601. }
  602. const payload = {
  603. acceptSharedPrepaid: !!this.form.acceptShare,
  604. customAmountEnabled,
  605. customMinAmount,
  606. tiers: this.form.tiers.map((tier) => ({
  607. rechargeAmount: Number(tier.amount),
  608. giftAmount: Number(tier.giftAmount) || 0,
  609. coupons: (tier.coupons || [])
  610. .filter((c) => Number(c.qty) > 0)
  611. .map((c) => ({
  612. couponId: c.id,
  613. quantity: Number(c.qty)
  614. }))
  615. }))
  616. };
  617. if (this.configVersion != null) {
  618. payload.version = this.configVersion;
  619. }
  620. this.saving = true;
  621. prepaidApi
  622. .saveRechargeConfig(payload)
  623. .then((res) => {
  624. if (res.data.code === 200) {
  625. uni.showToast({ title: '保存成功', icon: 'none' });
  626. setTimeout(() => {
  627. uni.navigateBack();
  628. }, 500);
  629. } else {
  630. uni.showToast({ title: res.data.message || '保存失败', icon: 'none' });
  631. }
  632. })
  633. .catch(() => {
  634. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  635. })
  636. .finally(() => {
  637. this.saving = false;
  638. });
  639. }
  640. }
  641. };
  642. </script>
  643. <style scoped lang="scss">
  644. .page {
  645. min-height: 100vh;
  646. background: #f5f5f5;
  647. padding: 24rpx;
  648. padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
  649. box-sizing: border-box;
  650. }
  651. .card {
  652. background: #fff;
  653. border-radius: 16rpx;
  654. margin-bottom: 24rpx;
  655. }
  656. .switch-card {
  657. display: flex;
  658. align-items: center;
  659. justify-content: space-between;
  660. padding: 28rpx 28rpx;
  661. .label {
  662. font-size: 28rpx;
  663. color: #333;
  664. flex: 1;
  665. padding-right: 24rpx;
  666. }
  667. }
  668. .min-card {
  669. padding: 28rpx 28rpx 32rpx;
  670. .min-head {
  671. display: flex;
  672. align-items: center;
  673. justify-content: space-between;
  674. margin-bottom: 28rpx;
  675. .label {
  676. font-size: 28rpx;
  677. color: #333;
  678. }
  679. .min-tip {
  680. font-size: 22rpx;
  681. color: #999;
  682. }
  683. }
  684. .min-input-wrap {
  685. display: flex;
  686. align-items: baseline;
  687. padding-bottom: 16rpx;
  688. border-bottom: 1px solid #e8e8e8;
  689. .currency {
  690. font-size: 40rpx;
  691. color: #999;
  692. margin-right: 8rpx;
  693. line-height: 1;
  694. }
  695. .min-input {
  696. flex: 1;
  697. font-size: 40rpx;
  698. color: #333;
  699. line-height: 1.2;
  700. }
  701. }
  702. }
  703. .placeholder {
  704. color: #bbb;
  705. font-size: 40rpx;
  706. }
  707. .section-head {
  708. margin: 8rpx 0 20rpx;
  709. padding: 0 4rpx;
  710. .section-title-row {
  711. display: flex;
  712. align-items: center;
  713. margin-bottom: 10rpx;
  714. }
  715. .section-bar {
  716. width: 8rpx;
  717. height: 28rpx;
  718. border-radius: 4rpx;
  719. background: #1c7cf9;
  720. margin-right: 12rpx;
  721. flex-shrink: 0;
  722. }
  723. .section-title {
  724. font-size: 30rpx;
  725. font-weight: 600;
  726. color: #333;
  727. }
  728. .section-tip {
  729. font-size: 22rpx;
  730. color: #999;
  731. padding-left: 20rpx;
  732. }
  733. }
  734. .tier-list {
  735. padding-bottom: 40rpx;
  736. }
  737. .tier-card {
  738. background: #fff;
  739. border-radius: 16rpx;
  740. padding: 28rpx;
  741. margin-bottom: 20rpx;
  742. .tier-top {
  743. display: flex;
  744. align-items: center;
  745. justify-content: space-between;
  746. margin-bottom: 8rpx;
  747. .tier-name {
  748. font-size: 30rpx;
  749. font-weight: 600;
  750. color: #333;
  751. }
  752. .tier-ops {
  753. display: flex;
  754. align-items: center;
  755. gap: 28rpx;
  756. .op {
  757. padding: 4rpx;
  758. }
  759. }
  760. }
  761. .tier-line {
  762. display: flex;
  763. justify-content: space-between;
  764. align-items: center;
  765. padding: 18rpx 0;
  766. border-bottom: 1px solid #f2f2f2;
  767. font-size: 26rpx;
  768. &:last-child {
  769. border-bottom: none;
  770. padding-bottom: 0;
  771. }
  772. .k {
  773. color: #999;
  774. }
  775. .v {
  776. color: #333;
  777. }
  778. }
  779. }
  780. .empty {
  781. padding: 80rpx 0;
  782. text-align: center;
  783. color: #ccc;
  784. font-size: 26rpx;
  785. }
  786. .fab {
  787. position: fixed;
  788. right: 40rpx;
  789. bottom: calc(170rpx + env(safe-area-inset-bottom));
  790. width: 60rpx;
  791. height: 60rpx;
  792. z-index: 20;
  793. }
  794. .footer {
  795. position: fixed;
  796. left: 0;
  797. right: 0;
  798. bottom: 0;
  799. padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
  800. background: transparent;
  801. z-index: 30;
  802. .save-btn {
  803. height: 88rpx;
  804. line-height: 88rpx;
  805. text-align: center;
  806. border-radius: 44rpx;
  807. background: #1c7cf9;
  808. color: #fff;
  809. font-size: 32rpx;
  810. font-weight: 500;
  811. &.disabled {
  812. background: #c0c4cc;
  813. }
  814. }
  815. &.sub-footer {
  816. position: relative;
  817. z-index: 1;
  818. flex-shrink: 0;
  819. background: #fff;
  820. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
  821. .save-btn {
  822. border-radius: 12rpx;
  823. background: #2979ff;
  824. }
  825. }
  826. &.tier-footer,
  827. &.coupon-footer {
  828. position: fixed;
  829. left: 0;
  830. right: 0;
  831. bottom: 0;
  832. padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
  833. background: transparent;
  834. box-shadow: none;
  835. .save-btn {
  836. border-radius: 44rpx;
  837. background: linear-gradient(90deg, #4da3ff 0%, #1c7cf9 100%);
  838. &.disabled {
  839. background: linear-gradient(90deg, rgba(77, 163, 255, 0.35) 0%, rgba(28, 124, 249, 0.35) 100%);
  840. color: rgba(255, 255, 255, 0.9);
  841. }
  842. }
  843. }
  844. }
  845. .sub-nav {
  846. display: flex;
  847. align-items: center;
  848. justify-content: center;
  849. position: relative;
  850. height: 88rpx;
  851. padding-top: var(--status-bar-height);
  852. background: #fff;
  853. border-bottom: 1px solid #f0f0f0;
  854. box-sizing: content-box;
  855. .sub-nav-back {
  856. position: absolute;
  857. left: 24rpx;
  858. bottom: 0;
  859. height: 88rpx;
  860. display: flex;
  861. align-items: center;
  862. padding: 0 12rpx;
  863. }
  864. .sub-nav-title {
  865. font-size: 32rpx;
  866. color: #333;
  867. font-weight: 500;
  868. line-height: 88rpx;
  869. }
  870. }
  871. .sub-page {
  872. width: 100vw;
  873. height: 100vh;
  874. background: #f5f5f5;
  875. display: flex;
  876. flex-direction: column;
  877. box-sizing: border-box;
  878. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  879. }
  880. .tier-page {
  881. .form-card {
  882. margin-top: 24rpx;
  883. }
  884. }
  885. .form-card {
  886. margin: 24rpx;
  887. background: #fff;
  888. border-radius: 16rpx;
  889. overflow: hidden;
  890. flex-shrink: 0;
  891. .form-title {
  892. padding: 28rpx 28rpx 8rpx;
  893. font-size: 30rpx;
  894. font-weight: 600;
  895. color: #333;
  896. }
  897. .form-row {
  898. display: flex;
  899. align-items: center;
  900. justify-content: space-between;
  901. padding: 28rpx;
  902. border-bottom: 1px solid #f2f2f2;
  903. &:last-child {
  904. border-bottom: none;
  905. }
  906. }
  907. .form-label {
  908. font-size: 28rpx;
  909. color: #333;
  910. width: 200rpx;
  911. flex-shrink: 0;
  912. }
  913. .form-input {
  914. flex: 1;
  915. text-align: right;
  916. font-size: 28rpx;
  917. color: #333;
  918. }
  919. .form-placeholder {
  920. color: #c0c4cc;
  921. font-size: 28rpx;
  922. }
  923. .link-row {
  924. .link-right {
  925. display: flex;
  926. align-items: center;
  927. gap: 8rpx;
  928. }
  929. .link-text {
  930. font-size: 28rpx;
  931. color: #c0c4cc;
  932. &.active {
  933. color: #333;
  934. }
  935. }
  936. }
  937. }
  938. .coupon-page {
  939. $coupon-orange: #ff5f3e;
  940. $ticket-left-w: 28%;
  941. .tabs {
  942. display: flex;
  943. background: #fff;
  944. border-bottom: 1px solid #f0f0f0;
  945. flex-shrink: 0;
  946. padding-bottom: 4rpx;
  947. .tab {
  948. flex: 1;
  949. display: flex;
  950. align-items: center;
  951. justify-content: center;
  952. height: 88rpx;
  953. position: relative;
  954. .tab-text {
  955. font-size: 28rpx;
  956. color: #999;
  957. line-height: 1.2;
  958. }
  959. .tab-indicator {
  960. position: absolute;
  961. left: 50%;
  962. bottom: 10rpx;
  963. transform: translateX(-50%);
  964. width: 48rpx;
  965. height: 12rpx;
  966. }
  967. &.active {
  968. .tab-text {
  969. color: #1c7cf9;
  970. font-weight: 600;
  971. }
  972. }
  973. }
  974. }
  975. .coupon-scroll {
  976. flex: 1;
  977. height: 0;
  978. padding: 24rpx;
  979. box-sizing: border-box;
  980. }
  981. .coupon-item {
  982. display: flex;
  983. align-items: center;
  984. background: #fff;
  985. border-radius: 16rpx;
  986. padding: 24rpx 20rpx;
  987. margin-bottom: 20rpx;
  988. gap: 18rpx;
  989. }
  990. .radio {
  991. width: 34rpx;
  992. height: 34rpx;
  993. border-radius: 50%;
  994. border: 2rpx solid #c8c8c8;
  995. display: flex;
  996. align-items: center;
  997. justify-content: center;
  998. flex-shrink: 0;
  999. box-sizing: border-box;
  1000. &.checked {
  1001. background: #1c7cf9;
  1002. border-color: #1c7cf9;
  1003. }
  1004. }
  1005. .coupon-ticket {
  1006. position: relative;
  1007. flex: 1;
  1008. min-width: 0;
  1009. display: flex;
  1010. align-items: stretch;
  1011. min-height: 180rpx;
  1012. box-sizing: border-box;
  1013. }
  1014. .ticket-bg {
  1015. position: absolute;
  1016. left: 0;
  1017. top: 0;
  1018. width: 100%;
  1019. height: 100%;
  1020. z-index: 0;
  1021. pointer-events: none;
  1022. }
  1023. .ticket-left {
  1024. position: relative;
  1025. z-index: 1;
  1026. width: $ticket-left-w;
  1027. flex-shrink: 0;
  1028. display: flex;
  1029. align-items: center;
  1030. justify-content: center;
  1031. padding: 24rpx 16rpx 24rpx 20rpx;
  1032. box-sizing: border-box;
  1033. }
  1034. .ticket-value {
  1035. font-size: 44rpx;
  1036. font-weight: 700;
  1037. color: $coupon-orange;
  1038. text-align: center;
  1039. line-height: 1.2;
  1040. word-break: break-all;
  1041. &.multi {
  1042. font-size: 26rpx;
  1043. line-height: 1.45;
  1044. white-space: pre-line;
  1045. }
  1046. }
  1047. .ticket-right {
  1048. position: relative;
  1049. z-index: 1;
  1050. flex: 1;
  1051. min-width: 0;
  1052. padding: 22rpx 24rpx 18rpx 28rpx;
  1053. display: flex;
  1054. flex-direction: column;
  1055. box-sizing: border-box;
  1056. }
  1057. .cname {
  1058. font-size: 28rpx;
  1059. font-weight: 600;
  1060. color: #333;
  1061. margin-bottom: 8rpx;
  1062. overflow: hidden;
  1063. text-overflow: ellipsis;
  1064. white-space: nowrap;
  1065. line-height: 1.3;
  1066. }
  1067. .ccond {
  1068. font-size: 24rpx;
  1069. color: $coupon-orange;
  1070. line-height: 1.4;
  1071. margin-bottom: 8rpx;
  1072. }
  1073. .ticket-bottom {
  1074. margin-top: auto;
  1075. display: flex;
  1076. align-items: center;
  1077. justify-content: space-between;
  1078. gap: 12rpx;
  1079. }
  1080. .cvalid {
  1081. flex: 1;
  1082. min-width: 0;
  1083. font-size: 22rpx;
  1084. color: #999;
  1085. line-height: 1.4;
  1086. overflow: hidden;
  1087. text-overflow: ellipsis;
  1088. white-space: nowrap;
  1089. }
  1090. .stepper {
  1091. display: flex;
  1092. align-items: center;
  1093. flex-shrink: 0;
  1094. gap: 12rpx;
  1095. .step-btn {
  1096. width: 36rpx;
  1097. height: 36rpx;
  1098. border-radius: 6rpx;
  1099. display: flex;
  1100. align-items: center;
  1101. justify-content: center;
  1102. box-sizing: border-box;
  1103. .step-icon {
  1104. font-size: 26rpx;
  1105. line-height: 1;
  1106. color: #fff;
  1107. font-weight: 500;
  1108. }
  1109. &.minus {
  1110. background: #d0d0d0;
  1111. &.active {
  1112. background: $coupon-orange;
  1113. }
  1114. }
  1115. &.plus {
  1116. background: $coupon-orange;
  1117. }
  1118. }
  1119. .step-num {
  1120. min-width: 28rpx;
  1121. text-align: center;
  1122. font-size: 28rpx;
  1123. color: #333;
  1124. line-height: 36rpx;
  1125. }
  1126. }
  1127. }
  1128. </style>