shareStoreAdd.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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, { getMerchantLocation, formatStoreDistance } 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. longitude: null,
  84. latitude: null
  85. };
  86. },
  87. computed: {
  88. tipsDisplayText() {
  89. return this.tipsExpanded ? TIPS_FULL : TIPS_SHORT;
  90. }
  91. },
  92. onLoad() {
  93. this.loadCandidates();
  94. },
  95. methods: {
  96. formatRating(item) {
  97. const score = Number(item.rating) || 0;
  98. const count = item.reviewCount;
  99. let countText = '';
  100. if (count === undefined || count === null || count === '') {
  101. countText = '暂无评论';
  102. } else if (typeof count === 'string') {
  103. countText = /评价|评论/.test(count) ? count : `${count}条评论`;
  104. } else {
  105. countText = `${count}条评论`;
  106. }
  107. return `${score.toFixed(1)}分 | ${countText}`;
  108. },
  109. mapStore(item) {
  110. return {
  111. id: item.merchantId || item.id,
  112. name: item.storeName || item.name || '',
  113. logo: item.headPhoto || item.logo || DEFAULT_LOGO,
  114. address: item.address || '',
  115. rating: Number(item.grade || item.rating || 0),
  116. reviewCount: item.commentNum ?? item.reviewCount ?? '',
  117. distance: formatStoreDistance(item)
  118. };
  119. },
  120. ensureLocation() {
  121. if (this.longitude != null && this.latitude != null) {
  122. return Promise.resolve({
  123. longitude: this.longitude,
  124. latitude: this.latitude
  125. });
  126. }
  127. return getMerchantLocation().then((loc) => {
  128. this.longitude = loc.longitude;
  129. this.latitude = loc.latitude;
  130. return loc;
  131. });
  132. },
  133. loadCandidates() {
  134. this.loading = true;
  135. this.ensureLocation()
  136. .then((loc) =>
  137. prepaidApi.getShareableStores({
  138. pageNo: 1,
  139. pageSize: 100,
  140. longitude: loc.longitude,
  141. latitude: loc.latitude
  142. })
  143. )
  144. .then((res) => {
  145. if (res.data.code !== 200) {
  146. uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
  147. return;
  148. }
  149. const page = res.data.result || {};
  150. const records = Array.isArray(page.records) ? page.records : [];
  151. this.candidateList = records.map((item) => this.mapStore(item));
  152. this.selectedIds = [];
  153. })
  154. .catch((err) => {
  155. this.candidateList = [];
  156. uni.showToast({
  157. title: (err && err.message) || '加载失败,请稍后重试',
  158. icon: 'none'
  159. });
  160. })
  161. .finally(() => {
  162. this.loading = false;
  163. });
  164. },
  165. isSelected(id) {
  166. return this.selectedIds.includes(id);
  167. },
  168. toggleSelect(item) {
  169. const idx = this.selectedIds.indexOf(item.id);
  170. if (idx >= 0) {
  171. this.selectedIds.splice(idx, 1);
  172. } else {
  173. this.selectedIds.push(item.id);
  174. }
  175. },
  176. onConfirm() {
  177. if (!this.selectedIds.length || this.submitting) return;
  178. this.submitting = true;
  179. prepaidApi
  180. .addSharedStores(this.selectedIds.slice())
  181. .then((res) => {
  182. if (res.data.code === 200) {
  183. uni.showToast({ title: '已添加共享门店', icon: 'none' });
  184. setTimeout(() => {
  185. uni.navigateBack();
  186. }, 400);
  187. } else {
  188. uni.showToast({ title: res.data.message || '添加失败', icon: 'none' });
  189. }
  190. })
  191. .catch(() => {
  192. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  193. })
  194. .finally(() => {
  195. this.submitting = false;
  196. });
  197. }
  198. }
  199. };
  200. </script>
  201. <style scoped lang="scss">
  202. .page {
  203. min-height: 100vh;
  204. background: #f7f7f7;
  205. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  206. box-sizing: border-box;
  207. }
  208. .list {
  209. padding: 24rpx 24rpx 0;
  210. }
  211. .store-card {
  212. display: flex;
  213. align-items: center;
  214. background: #fff;
  215. border-radius: 16rpx;
  216. padding: 24rpx;
  217. margin-bottom: 20rpx;
  218. .checkbox {
  219. width: 36rpx;
  220. height: 36rpx;
  221. border-radius: 50%;
  222. border: 2rpx solid #ccc;
  223. display: flex;
  224. align-items: center;
  225. justify-content: center;
  226. flex-shrink: 0;
  227. margin-right: 16rpx;
  228. box-sizing: border-box;
  229. &.checked {
  230. background: #1C7CF9;
  231. border-color: #1C7CF9;
  232. }
  233. }
  234. .logo {
  235. width: 120rpx;
  236. height: 120rpx;
  237. border-radius: 12rpx;
  238. background: #f0f0f0;
  239. flex-shrink: 0;
  240. margin-right: 20rpx;
  241. }
  242. .info {
  243. flex: 1;
  244. min-width: 0;
  245. .name {
  246. font-size: 30rpx;
  247. font-weight: 600;
  248. color: #333;
  249. line-height: 1.35;
  250. margin-bottom: 10rpx;
  251. }
  252. .meta {
  253. display: flex;
  254. align-items: center;
  255. gap: 8rpx;
  256. margin-bottom: 10rpx;
  257. .stars {
  258. display: flex;
  259. align-items: center;
  260. gap: 2rpx;
  261. }
  262. .meta-text {
  263. font-size: 22rpx;
  264. color: #FF4D4F;
  265. line-height: 1.2;
  266. }
  267. }
  268. .addr-row {
  269. display: flex;
  270. align-items: center;
  271. gap: 12rpx;
  272. .addr {
  273. flex: 1;
  274. min-width: 0;
  275. font-size: 22rpx;
  276. color: #999;
  277. line-height: 1.4;
  278. }
  279. .distance {
  280. flex-shrink: 0;
  281. font-size: 22rpx;
  282. color: #999;
  283. line-height: 1.4;
  284. }
  285. }
  286. }
  287. }
  288. .tips-box {
  289. margin: 8rpx 24rpx 24rpx;
  290. padding: 24rpx;
  291. background: #fff;
  292. border-radius: 16rpx;
  293. .tips-title {
  294. font-size: 28rpx;
  295. font-weight: 600;
  296. color: #333;
  297. margin-bottom: 16rpx;
  298. }
  299. .tips-content {
  300. .tips-text {
  301. font-size: 24rpx;
  302. color: #999;
  303. line-height: 1.7;
  304. white-space: pre-wrap;
  305. }
  306. &.collapsed {
  307. display: -webkit-box;
  308. -webkit-box-orient: vertical;
  309. -webkit-line-clamp: 3;
  310. overflow: hidden;
  311. }
  312. }
  313. .tips-toggle {
  314. display: flex;
  315. align-items: center;
  316. justify-content: center;
  317. gap: 6rpx;
  318. margin-top: 16rpx;
  319. font-size: 24rpx;
  320. color: #1C7CF9;
  321. }
  322. }
  323. .empty-wrap {
  324. padding-top: 120rpx;
  325. display: flex;
  326. justify-content: center;
  327. }
  328. .footer {
  329. position: fixed;
  330. left: 0;
  331. right: 0;
  332. bottom: 0;
  333. padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
  334. background: #fff;
  335. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
  336. z-index: 20;
  337. .confirm-btn {
  338. height: 88rpx;
  339. line-height: 88rpx;
  340. text-align: center;
  341. border-radius: 160rpx;
  342. background: linear-gradient(269deg, #2260F6 0%, #0FB0FF 100%);
  343. color: #fff;
  344. font-size: 32rpx;
  345. font-weight: 500;
  346. &.disabled {
  347. background: #B7D4FF;
  348. color: #fff;
  349. }
  350. }
  351. }
  352. </style>