manage.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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="enabled" activeColor="#2979ff" :disabled="toggling" @change="onToggle"></u-switch>
  8. </view>
  9. <!-- 数据指标 -->
  10. <view class="stats-card" :style="{ backgroundImage: 'url(' + statsBg + ')' }">
  11. <view class="balance-block">
  12. <text class="balance-label">储值余额(元)</text>
  13. <text class="balance-value">¥ {{ balanceText }}</text>
  14. </view>
  15. <view class="stats-panel">
  16. <view class="stat" v-for="(item, index) in stats" :key="index">
  17. <view class="value">{{ item.value }}</view>
  18. <view class="name">{{ item.name }}</view>
  19. </view>
  20. </view>
  21. </view>
  22. <!-- 操作入口 -->
  23. <view class="actions card">
  24. <view class="action-btn" @click="goSettings">
  25. <image class="action-icon" :src="iconSettings" mode="aspectFit"></image>
  26. <text class="action-text">储值设置</text>
  27. <image class="action-more" :src="iconMore" mode="aspectFit"></image>
  28. </view>
  29. <view class="action-divider"></view>
  30. <view class="action-btn" @click="goShareStore">
  31. <image class="action-icon" :src="iconShareStore" mode="aspectFit"></image>
  32. <text class="action-text">共享门店</text>
  33. <image class="action-more" :src="iconMore" mode="aspectFit"></image>
  34. </view>
  35. </view>
  36. <!-- 账户明细 -->
  37. <view class="card detail-card">
  38. <view class="detail-head">
  39. <text class="title">账户明细</text>
  40. <view class="more" @click="goAll">
  41. 全部
  42. <u-icon name="arrow-right" color="#999" size="12"></u-icon>
  43. </view>
  44. </view>
  45. <view class="list" v-if="records.length">
  46. <view class="item" v-for="(item, index) in records" :key="item.id || index" @click="goDetail(item)">
  47. <image class="left-icon" :src="getTypeIcon(item.type)" mode="aspectFit"></image>
  48. <view class="info">
  49. <view class="row1">
  50. <text class="name">{{ item.title }}</text>
  51. <text class="amount" :class="{ plus: item.amount > 0 }">
  52. {{ item.amount > 0 ? '+' : '' }}{{ formatMoney(item.amount) }}
  53. </text>
  54. </view>
  55. <view class="row2">
  56. <text class="store">{{ item.storeName }}</text>
  57. <text class="time">{{ item.time }}</text>
  58. </view>
  59. </view>
  60. </view>
  61. </view>
  62. <view class="empty" v-else>暂无明细</view>
  63. </view>
  64. <!-- 开启/关闭确认弹窗 -->
  65. <u-popup v-model="confirmVisible" mode="center" border-radius="24" :mask-close-able="false">
  66. <view class="confirm-dialog">
  67. <view class="confirm-title">提示</view>
  68. <view class="confirm-body">
  69. <text class="confirm-text">{{ confirmContent }}</text>
  70. </view>
  71. <view class="confirm-footer">
  72. <view class="btn cancel" @click="onConfirmCancel">取消</view>
  73. <view class="btn ok" @click="onConfirmOk">确定</view>
  74. </view>
  75. </view>
  76. </u-popup>
  77. </view>
  78. </template>
  79. <script>
  80. import prepaidApi, { getMerchantId } from '@/api/memberPrepaid.js'
  81. export default {
  82. data() {
  83. return {
  84. enabled: false,
  85. toggling: false,
  86. confirmVisible: false,
  87. confirmContent: '',
  88. pendingEnabled: null,
  89. balance: 0,
  90. balanceText: '0.00',
  91. statsBg: '/static/topUp/indexBg.png',
  92. iconSettings: '/static/topUp/prepaidsettings.png',
  93. iconShareStore: '/static/topUp/sharedstores.png',
  94. iconMore: '/static/topUp/more.png',
  95. iconRecharge: '/static/topUp/recharge.png',
  96. iconSpend: '/static/topUp/expenditure.png',
  97. iconGive: '/static/topUp/give.png',
  98. stats: [
  99. { name: '储值总额(元)', value: '0.00' },
  100. { name: '储值总用户', value: '0' },
  101. { name: '储值余额用户', value: '0' }
  102. ],
  103. records: []
  104. };
  105. },
  106. onShow() {
  107. this.bootstrap();
  108. },
  109. methods: {
  110. formatMoney(val) {
  111. const n = Number(val);
  112. if (Number.isNaN(n)) return '0.00';
  113. return n.toFixed(2);
  114. },
  115. formatCount(val) {
  116. return String(Number(val) || 0);
  117. },
  118. getTypeIcon(type) {
  119. if (type === 'spend') return this.iconSpend;
  120. if (type === 'gift') return this.iconGive;
  121. return this.iconRecharge;
  122. },
  123. mapLedgerItem(item) {
  124. const txType = String(item.transactionType || '').toUpperCase();
  125. const isRecharge = txType === 'RECHARGE';
  126. const isGift = txType === 'GIFT' || txType === 'GIVE';
  127. const userName = item.userName || '用户';
  128. const changeAmount = Number(item.changeAmount);
  129. let type = 'spend';
  130. let titlePrefix = '支出';
  131. if (isGift) {
  132. type = 'gift';
  133. titlePrefix = '赠送';
  134. } else if (isRecharge) {
  135. type = 'recharge';
  136. titlePrefix = '充值';
  137. }
  138. return {
  139. id: item.ledgerId,
  140. type,
  141. title: `${titlePrefix}-${userName}`,
  142. storeName: item.storeName || '',
  143. amount: Number.isNaN(changeAmount)
  144. ? isRecharge || isGift
  145. ? Number(item.payAmount || 0) + Number(item.giftAmount || 0)
  146. : -Math.abs(Number(item.payAmount || 0))
  147. : changeAmount,
  148. time: item.transactionTime || ''
  149. };
  150. },
  151. bootstrap() {
  152. const merchantId = getMerchantId();
  153. if (!merchantId) {
  154. uni.showToast({ title: '未获取到门店信息', icon: 'none' });
  155. return;
  156. }
  157. prepaidApi.getFeatureStatus(merchantId).then((res) => {
  158. if (res.data.code !== 200) {
  159. uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
  160. return;
  161. }
  162. const data = res.data.result || {};
  163. const auditStatus = Number(data.auditStatus);
  164. if (auditStatus !== 2) {
  165. uni.redirectTo({ url: '/pages/topUp/index' });
  166. return;
  167. }
  168. this.enabled = Number(data.operationStatus) === 1;
  169. this.loadOverview(merchantId);
  170. });
  171. },
  172. loadOverview(merchantId) {
  173. prepaidApi.getOverview(merchantId).then((res) => {
  174. if (res.data.code !== 200 || !res.data.result) return;
  175. const data = res.data.result;
  176. console.log('11111data',data)
  177. this.balance = Number(data.balance) || 0;
  178. this.balanceText = this.formatMoney(data.balance);
  179. this.stats = [
  180. { name: '储值总额(元)', value: this.formatMoney(data.totalRechargeAmount) },
  181. { name: '储值总用户', value: this.formatCount(data.totalUserCount) },
  182. { name: '储值余额用户', value: this.formatCount(data.balanceUserCount) }
  183. ];
  184. const list = Array.isArray(data.latestLedgers) ? data.latestLedgers : [];
  185. this.records = list.map((item) => this.mapLedgerItem(item));
  186. });
  187. },
  188. onToggle(val) {
  189. this.enabled = !val;
  190. this.pendingEnabled = val;
  191. if (val) {
  192. this.confirmContent = '是否确认开启门店储值功能?';
  193. } else if (this.balance > 0) {
  194. this.confirmContent = '用户账户余额未使用完是否确认关闭门店储值功能?';
  195. } else {
  196. this.confirmContent = '是否确认关闭门店储值功能?';
  197. }
  198. this.confirmVisible = true;
  199. },
  200. onConfirmCancel() {
  201. this.confirmVisible = false;
  202. this.pendingEnabled = null;
  203. },
  204. onConfirmOk() {
  205. const val = this.pendingEnabled;
  206. this.confirmVisible = false;
  207. this.pendingEnabled = null;
  208. if (val === null || val === undefined || this.toggling) return;
  209. const merchantId = getMerchantId();
  210. this.toggling = true;
  211. const req = val
  212. ? prepaidApi.openFeature(merchantId)
  213. : prepaidApi.closeFeature({}, merchantId);
  214. req
  215. .then((res) => {
  216. if (res.data.code === 200) {
  217. const data = res.data.result || {};
  218. this.enabled = Number(data.operationStatus) === 1;
  219. uni.showToast({
  220. title: this.enabled ? '已开启储值功能' : '已关闭储值功能',
  221. icon: 'none'
  222. });
  223. } else {
  224. uni.showToast({ title: res.data.message || '操作失败', icon: 'none' });
  225. }
  226. })
  227. .catch(() => {
  228. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  229. })
  230. .finally(() => {
  231. this.toggling = false;
  232. });
  233. },
  234. goSettings() {
  235. uni.navigateTo({ url: '/pages/topUp/settings' });
  236. },
  237. goShareStore() {
  238. uni.navigateTo({ url: '/pages/topUp/shareStore' });
  239. },
  240. goAll() {
  241. uni.navigateTo({ url: '/pages/topUp/records' });
  242. },
  243. goDetail(item) {
  244. const type = item.type === 'spend' ? 'spend' : 'recharge';
  245. uni.navigateTo({
  246. url: `/pages/topUp/recordDetail?id=${item.id || ''}&type=${type}`
  247. });
  248. }
  249. }
  250. };
  251. </script>
  252. <style scoped lang="scss">
  253. .page {
  254. min-height: 100vh;
  255. background: #f5f6f8;
  256. padding: 24rpx;
  257. padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  258. }
  259. .card {
  260. background: #fff;
  261. border-radius: 16rpx;
  262. margin-bottom: 24rpx;
  263. }
  264. .switch-card {
  265. display: flex;
  266. align-items: center;
  267. justify-content: space-between;
  268. padding: 28rpx 32rpx;
  269. .label {
  270. font-size: 30rpx;
  271. color: #333;
  272. font-weight: 500;
  273. }
  274. }
  275. .stats-card {
  276. border-radius: 20rpx;
  277. margin-bottom: 24rpx;
  278. padding: 36rpx 28rpx 24rpx;
  279. background-color: #3a9bff;
  280. background-repeat: no-repeat;
  281. background-size: 100% 100%;
  282. background-position: center;
  283. overflow: hidden;
  284. box-sizing: border-box;
  285. .balance-block {
  286. padding: 8rpx 8rpx 32rpx;
  287. .balance-label {
  288. display: block;
  289. font-size: 26rpx;
  290. color: rgba(255, 255, 255, 0.9);
  291. margin-bottom: 16rpx;
  292. }
  293. .balance-value {
  294. display: block;
  295. font-size: 56rpx;
  296. font-weight: 600;
  297. color: #fff;
  298. line-height: 1.2;
  299. text-align: center;
  300. }
  301. }
  302. .stats-panel {
  303. display: flex;
  304. align-items: stretch;
  305. background: #fff;
  306. border-radius: 16rpx;
  307. padding: 28rpx 8rpx;
  308. .stat {
  309. flex: 1;
  310. text-align: center;
  311. position: relative;
  312. &:not(:last-child)::after {
  313. content: '';
  314. position: absolute;
  315. right: 0;
  316. top: 50%;
  317. transform: translateY(-50%);
  318. width: 1px;
  319. height: 56rpx;
  320. background: #f0f0f0;
  321. }
  322. .value {
  323. font-size: 32rpx;
  324. font-weight: 600;
  325. color: #333;
  326. line-height: 1.3;
  327. }
  328. .name {
  329. font-size: 22rpx;
  330. color: #999;
  331. margin-top: 10rpx;
  332. }
  333. }
  334. }
  335. }
  336. .actions {
  337. display: flex;
  338. align-items: center;
  339. padding: 8rpx 0;
  340. margin-bottom: 24rpx;
  341. .action-btn {
  342. flex: 1;
  343. display: flex;
  344. align-items: center;
  345. justify-content: center;
  346. padding: 28rpx 16rpx;
  347. gap: 12rpx;
  348. }
  349. .action-icon {
  350. width: 44rpx;
  351. height: 44rpx;
  352. flex-shrink: 0;
  353. }
  354. .action-text {
  355. font-size: 28rpx;
  356. color: #333;
  357. font-weight: 500;
  358. }
  359. .action-more {
  360. width: 28rpx;
  361. height: 28rpx;
  362. flex-shrink: 0;
  363. }
  364. .action-divider {
  365. width: 1px;
  366. height: 40rpx;
  367. background: #ebebeb;
  368. flex-shrink: 0;
  369. }
  370. }
  371. .detail-card {
  372. padding: 28rpx 24rpx 8rpx;
  373. .detail-head {
  374. display: flex;
  375. align-items: center;
  376. justify-content: space-between;
  377. margin-bottom: 8rpx;
  378. .title {
  379. font-size: 30rpx;
  380. font-weight: 600;
  381. color: #333;
  382. }
  383. .more {
  384. display: flex;
  385. align-items: center;
  386. font-size: 24rpx;
  387. color: #999;
  388. gap: 4rpx;
  389. }
  390. }
  391. .item {
  392. display: flex;
  393. align-items: center;
  394. padding: 28rpx 0;
  395. border-bottom: 1px solid #f5f5f5;
  396. &:last-child {
  397. border-bottom: none;
  398. }
  399. .left-icon {
  400. width: 72rpx;
  401. height: 72rpx;
  402. margin-right: 20rpx;
  403. flex-shrink: 0;
  404. border-radius: 50%;
  405. }
  406. .info {
  407. flex: 1;
  408. min-width: 0;
  409. }
  410. .row1,
  411. .row2 {
  412. display: flex;
  413. justify-content: space-between;
  414. align-items: center;
  415. }
  416. .row1 {
  417. margin-bottom: 10rpx;
  418. .name {
  419. font-size: 28rpx;
  420. color: #333;
  421. font-weight: 500;
  422. overflow: hidden;
  423. text-overflow: ellipsis;
  424. white-space: nowrap;
  425. max-width: 360rpx;
  426. }
  427. .amount {
  428. font-size: 30rpx;
  429. font-weight: 600;
  430. color: #333;
  431. flex-shrink: 0;
  432. &.plus {
  433. color: #2f7bff;
  434. }
  435. }
  436. }
  437. .row2 {
  438. .store,
  439. .time {
  440. font-size: 22rpx;
  441. color: #999;
  442. }
  443. .store {
  444. overflow: hidden;
  445. text-overflow: ellipsis;
  446. white-space: nowrap;
  447. max-width: 360rpx;
  448. }
  449. }
  450. }
  451. .empty {
  452. padding: 60rpx 0;
  453. text-align: center;
  454. color: #ccc;
  455. font-size: 26rpx;
  456. }
  457. }
  458. .confirm-dialog {
  459. width: 560rpx;
  460. background: #fff;
  461. border-radius: 24rpx;
  462. overflow: hidden;
  463. padding: 48rpx 40rpx 40rpx;
  464. box-sizing: border-box;
  465. .confirm-title {
  466. text-align: center;
  467. font-size: 34rpx;
  468. font-weight: 600;
  469. color: #333;
  470. line-height: 1.4;
  471. margin-bottom: 28rpx;
  472. }
  473. .confirm-body {
  474. padding: 0 8rpx 40rpx;
  475. .confirm-text {
  476. display: block;
  477. text-align: center;
  478. font-size: 28rpx;
  479. color: #333;
  480. line-height: 1.6;
  481. }
  482. }
  483. .confirm-footer {
  484. display: flex;
  485. align-items: center;
  486. justify-content: space-between;
  487. gap: 24rpx;
  488. .btn {
  489. flex: 1;
  490. height: 80rpx;
  491. line-height: 80rpx;
  492. text-align: center;
  493. border-radius: 40rpx;
  494. font-size: 30rpx;
  495. box-sizing: border-box;
  496. }
  497. .cancel {
  498. color: #999;
  499. background: #fff;
  500. border: 2rpx solid #dcdcdc;
  501. }
  502. .ok {
  503. color: #fff;
  504. background: linear-gradient(90deg, #3ab3ff 0%, #2472ff 100%);
  505. border: none;
  506. }
  507. }
  508. }
  509. </style>