shareStoreAdd.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <template>
  2. <view class="page">
  3. <u-navbar title="共享门店" :autoBack="true" :placeholder="true"></u-navbar>
  4. <!-- 可选门店列表 -->
  5. <view class="list" v-if="candidateList.length">
  6. <view
  7. class="store-card"
  8. v-for="item in candidateList"
  9. :key="item.id"
  10. @click="toggleSelect(item)"
  11. >
  12. <view class="checkbox" :class="{ checked: isSelected(item.id) }">
  13. <u-icon v-if="isSelected(item.id)" name="checkmark" color="#fff" size="14"></u-icon>
  14. </view>
  15. <image class="logo" :src="item.logo || defaultLogo" mode="aspectFill"></image>
  16. <view class="info">
  17. <view class="name u-line-1">{{ item.name }}</view>
  18. <view class="meta">
  19. <view class="stars">
  20. <u-icon
  21. v-for="n in 5"
  22. :key="n"
  23. name="heart-fill"
  24. :color="n <= Math.round(item.rating) ? '#FF4D4F' : '#E5E5E5'"
  25. size="20"
  26. ></u-icon>
  27. </view>
  28. <text class="meta-text">{{ formatRating(item) }}</text>
  29. </view>
  30. <view class="addr-row">
  31. <text class="addr u-line-1">{{ item.address || '暂无地址' }}</text>
  32. <text class="distance" v-if="item.distance">距您{{ item.distance }}</text>
  33. </view>
  34. </view>
  35. </view>
  36. </view>
  37. <view class="empty-wrap" v-else-if="!loading">
  38. <u-empty text="暂无可添加门店" mode="list"></u-empty>
  39. </view>
  40. <view class="empty-wrap" v-else>
  41. <u-loading mode="circle" size="48"></u-loading>
  42. </view>
  43. <!-- 数据说明 -->
  44. <view class="tips-box" v-if="candidateList.length || !loading">
  45. <view class="tips-title">数据说明</view>
  46. <view class="tips-content" :class="{ collapsed: !tipsExpanded }">
  47. <text class="tips-text">{{ tipsDisplayText }}</text>
  48. </view>
  49. <view class="tips-toggle" @click="tipsExpanded = !tipsExpanded">
  50. <text>{{ tipsExpanded ? '收起' : '展开更多' }}</text>
  51. <u-icon :name="tipsExpanded ? 'arrow-up' : 'arrow-down'" color="#1C7CF9" size="12"></u-icon>
  52. </view>
  53. </view>
  54. <view class="footer">
  55. <view
  56. class="confirm-btn"
  57. :class="{ disabled: !selectedIds.length || submitting }"
  58. @click="onConfirm"
  59. >
  60. {{ submitting ? '提交中...' : '确认并添加' }}
  61. </view>
  62. </view>
  63. </view>
  64. </template>
  65. <script>
  66. import prepaidApi from '@/api/memberPrepaid.js'
  67. const DEFAULT_LOGO = '/static/index/shop.png'
  68. const TIPS_SHORT =
  69. '1、共享门店由对方商户自主开启权限控制,未开启权限的门店无法添加;若对方后续关闭权限,该门店将自动从共享列表中移除。'
  70. const TIPS_FULL =
  71. '1、共享门店由对方商户自主开启权限控制,未开启权限的门店无法添加;若对方后续关闭权限,该门店将自动从共享列表中移除。\n' +
  72. '2、添加共享后,储值本金可在所选门店通用消费;充值赠送的优惠券仅限原充值门店使用,不可跨店核销。\n' +
  73. '3、勾选门店并点击「确认并添加」后生效;结算时优先扣除共享储值余额。可随时取消共享,不影响已产生的订单。'
  74. export default {
  75. data() {
  76. return {
  77. defaultLogo: DEFAULT_LOGO,
  78. tipsExpanded: false,
  79. candidateList: [],
  80. selectedIds: [],
  81. loading: false,
  82. submitting: false
  83. };
  84. },
  85. computed: {
  86. tipsDisplayText() {
  87. return this.tipsExpanded ? TIPS_FULL : TIPS_SHORT;
  88. }
  89. },
  90. onLoad() {
  91. this.loadCandidates();
  92. },
  93. methods: {
  94. formatRating(item) {
  95. const score = Number(item.rating) || 0;
  96. const count = item.reviewCount;
  97. let countText = '';
  98. if (count === undefined || count === null || count === '') {
  99. countText = '暂无评论';
  100. } else if (typeof count === 'string') {
  101. countText = /评价|评论/.test(count) ? count : `${count}条评论`;
  102. } else {
  103. countText = `${count}条评论`;
  104. }
  105. return `${score.toFixed(1)}分 | ${countText}`;
  106. },
  107. mapStore(item) {
  108. return {
  109. id: item.merchantId || item.id,
  110. name: item.storeName || item.name || '',
  111. logo: item.headPhoto || item.logo || DEFAULT_LOGO,
  112. address: item.address || '',
  113. rating: Number(item.grade || item.rating || 0),
  114. reviewCount: item.commentNum ?? item.reviewCount ?? '',
  115. distance: item.distance || item.distanceText || ''
  116. };
  117. },
  118. loadCandidates() {
  119. this.loading = true;
  120. prepaidApi
  121. .getShareableStores({ pageNo: 1, pageSize: 100 })
  122. .then((res) => {
  123. if (res.data.code !== 200) {
  124. uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
  125. return;
  126. }
  127. const page = res.data.result || {};
  128. const records = Array.isArray(page.records) ? page.records : [];
  129. this.candidateList = records.map((item) => this.mapStore(item));
  130. this.selectedIds = [];
  131. })
  132. .catch(() => {
  133. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  134. })
  135. .finally(() => {
  136. this.loading = false;
  137. });
  138. },
  139. isSelected(id) {
  140. return this.selectedIds.includes(id);
  141. },
  142. toggleSelect(item) {
  143. const idx = this.selectedIds.indexOf(item.id);
  144. if (idx >= 0) {
  145. this.selectedIds.splice(idx, 1);
  146. } else {
  147. this.selectedIds.push(item.id);
  148. }
  149. },
  150. onConfirm() {
  151. if (!this.selectedIds.length || this.submitting) return;
  152. this.submitting = true;
  153. prepaidApi
  154. .addSharedStores(this.selectedIds.slice())
  155. .then((res) => {
  156. if (res.data.code === 200) {
  157. uni.showToast({ title: '已添加共享门店', icon: 'none' });
  158. setTimeout(() => {
  159. uni.navigateBack();
  160. }, 400);
  161. } else {
  162. uni.showToast({ title: res.data.message || '添加失败', icon: 'none' });
  163. }
  164. })
  165. .catch(() => {
  166. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  167. })
  168. .finally(() => {
  169. this.submitting = false;
  170. });
  171. }
  172. }
  173. };
  174. </script>
  175. <style scoped lang="scss">
  176. .page {
  177. min-height: 100vh;
  178. background: #f7f7f7;
  179. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  180. box-sizing: border-box;
  181. }
  182. .list {
  183. padding: 24rpx 24rpx 0;
  184. }
  185. .store-card {
  186. display: flex;
  187. align-items: center;
  188. background: #fff;
  189. border-radius: 16rpx;
  190. padding: 24rpx;
  191. margin-bottom: 20rpx;
  192. .checkbox {
  193. width: 36rpx;
  194. height: 36rpx;
  195. border-radius: 50%;
  196. border: 2rpx solid #ccc;
  197. display: flex;
  198. align-items: center;
  199. justify-content: center;
  200. flex-shrink: 0;
  201. margin-right: 16rpx;
  202. box-sizing: border-box;
  203. &.checked {
  204. background: #1C7CF9;
  205. border-color: #1C7CF9;
  206. }
  207. }
  208. .logo {
  209. width: 120rpx;
  210. height: 120rpx;
  211. border-radius: 12rpx;
  212. background: #f0f0f0;
  213. flex-shrink: 0;
  214. margin-right: 20rpx;
  215. }
  216. .info {
  217. flex: 1;
  218. min-width: 0;
  219. .name {
  220. font-size: 30rpx;
  221. font-weight: 600;
  222. color: #333;
  223. line-height: 1.35;
  224. margin-bottom: 10rpx;
  225. }
  226. .meta {
  227. display: flex;
  228. align-items: center;
  229. gap: 8rpx;
  230. margin-bottom: 10rpx;
  231. .stars {
  232. display: flex;
  233. align-items: center;
  234. gap: 2rpx;
  235. }
  236. .meta-text {
  237. font-size: 22rpx;
  238. color: #FF4D4F;
  239. line-height: 1.2;
  240. }
  241. }
  242. .addr-row {
  243. display: flex;
  244. align-items: center;
  245. gap: 12rpx;
  246. .addr {
  247. flex: 1;
  248. min-width: 0;
  249. font-size: 22rpx;
  250. color: #999;
  251. line-height: 1.4;
  252. }
  253. .distance {
  254. flex-shrink: 0;
  255. font-size: 22rpx;
  256. color: #999;
  257. line-height: 1.4;
  258. }
  259. }
  260. }
  261. }
  262. .tips-box {
  263. margin: 8rpx 24rpx 24rpx;
  264. padding: 24rpx;
  265. background: #fff;
  266. border-radius: 16rpx;
  267. .tips-title {
  268. font-size: 28rpx;
  269. font-weight: 600;
  270. color: #333;
  271. margin-bottom: 16rpx;
  272. }
  273. .tips-content {
  274. .tips-text {
  275. font-size: 24rpx;
  276. color: #999;
  277. line-height: 1.7;
  278. white-space: pre-wrap;
  279. }
  280. &.collapsed {
  281. display: -webkit-box;
  282. -webkit-box-orient: vertical;
  283. -webkit-line-clamp: 3;
  284. overflow: hidden;
  285. }
  286. }
  287. .tips-toggle {
  288. display: flex;
  289. align-items: center;
  290. justify-content: center;
  291. gap: 6rpx;
  292. margin-top: 16rpx;
  293. font-size: 24rpx;
  294. color: #1C7CF9;
  295. }
  296. }
  297. .empty-wrap {
  298. padding-top: 120rpx;
  299. display: flex;
  300. justify-content: center;
  301. }
  302. .footer {
  303. position: fixed;
  304. left: 0;
  305. right: 0;
  306. bottom: 0;
  307. padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
  308. background: #fff;
  309. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
  310. z-index: 20;
  311. .confirm-btn {
  312. height: 88rpx;
  313. line-height: 88rpx;
  314. text-align: center;
  315. border-radius: 160rpx;
  316. background: linear-gradient(269deg, #2260F6 0%, #0FB0FF 100%);
  317. color: #fff;
  318. font-size: 32rpx;
  319. font-weight: 500;
  320. &.disabled {
  321. background: #B7D4FF;
  322. color: #fff;
  323. }
  324. }
  325. }
  326. </style>