settings.vue 30 KB

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