shareStore.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <template>
  2. <view class="page">
  3. <u-navbar title="共享门店" :autoBack="true" :placeholder="true"></u-navbar>
  4. <view class="list" v-if="storeList.length">
  5. <view class="store-card" v-for="item in storeList" :key="item.id">
  6. <view class="del-btn" @click.stop="onDelete(item)">
  7. <image class="del-bg" src="/static/topUp/del.png" mode="scaleToFill"></image>
  8. <text class="del-text">删除</text>
  9. </view>
  10. <image class="logo" :src="item.logo || defaultLogo" mode="aspectFill"></image>
  11. <view class="info">
  12. <view class="name u-line-1">{{ item.name }}</view>
  13. <view class="meta">
  14. <view class="stars">
  15. <u-icon
  16. v-for="n in 5"
  17. :key="n"
  18. name="heart-fill"
  19. :color="n <= Math.round(item.rating) ? '#FF4D4F' : '#E5E5E5'"
  20. size="20"
  21. ></u-icon>
  22. </view>
  23. <text class="meta-text">{{ formatRating(item) }}</text>
  24. </view>
  25. <view class="addr u-line-1">{{ item.address || '暂无地址' }}</view>
  26. <view class="distance" v-if="item.distance">距您{{ item.distance }}</view>
  27. </view>
  28. </view>
  29. </view>
  30. <view class="empty-wrap" v-else-if="loading">
  31. <u-loading mode="circle" size="48"></u-loading>
  32. <text class="empty-text">加载中...</text>
  33. </view>
  34. <view class="empty-wrap" v-else>
  35. <u-empty text="暂无数据" mode="list"></u-empty>
  36. </view>
  37. <view class="footer">
  38. <view class="add-btn" :class="{ disabled: !canAdd }" @click="goAdd">添加</view>
  39. </view>
  40. <!-- 删除确认弹窗 -->
  41. <u-popup v-model="confirmVisible" mode="center" border-radius="24" :mask-close-able="false">
  42. <view class="confirm-dialog">
  43. <view class="confirm-title">提示</view>
  44. <view class="confirm-body">
  45. <text class="confirm-text">是否确认删除该共享门店?</text>
  46. </view>
  47. <view class="confirm-footer">
  48. <view class="btn cancel" @click="onConfirmCancel">取消</view>
  49. <view class="btn ok" @click="confirmDelete">确定</view>
  50. </view>
  51. </view>
  52. </u-popup>
  53. </view>
  54. </template>
  55. <script>
  56. import prepaidApi, { getMerchantId, getMerchantLocation, formatStoreDistance } from '@/api/memberPrepaid.js'
  57. const DEFAULT_LOGO = '/static/index/shop.png'
  58. export default {
  59. data() {
  60. return {
  61. defaultLogo: DEFAULT_LOGO,
  62. canAdd: false,
  63. loading: false,
  64. storeList: [],
  65. confirmVisible: false,
  66. pendingDelete: null,
  67. deleting: false,
  68. longitude: null,
  69. latitude: null
  70. };
  71. },
  72. onShow() {
  73. this.confirmVisible = false;
  74. this.pendingDelete = null;
  75. this.refreshEnabled();
  76. this.loadList();
  77. },
  78. methods: {
  79. formatRating(item) {
  80. const score = Number(item.rating) || 0;
  81. const count = item.reviewCount;
  82. let countText = '';
  83. if (count === undefined || count === null || count === '') {
  84. countText = '暂无评论';
  85. } else if (typeof count === 'string') {
  86. countText = /评价|评论/.test(count) ? count : `${count}条评论`;
  87. } else {
  88. countText = `${count}条评论`;
  89. }
  90. return `${score.toFixed(1)}分 | ${countText}`;
  91. },
  92. mapStore(item) {
  93. return {
  94. id: item.merchantId || item.id,
  95. name: item.storeName || item.name || '',
  96. logo: item.headPhoto || item.logo || DEFAULT_LOGO,
  97. address: item.address || '',
  98. rating: Number(item.grade || item.rating || 0),
  99. reviewCount: item.commentNum ?? item.reviewCount ?? '',
  100. distance: formatStoreDistance(item)
  101. };
  102. },
  103. parseRecords(result) {
  104. if (!result) return [];
  105. if (Array.isArray(result)) return result;
  106. if (Array.isArray(result.records)) return result.records;
  107. if (result.page && Array.isArray(result.page.records)) return result.page.records;
  108. return [];
  109. },
  110. refreshEnabled() {
  111. const merchantId = getMerchantId();
  112. if (!merchantId) {
  113. this.canAdd = false;
  114. return;
  115. }
  116. prepaidApi.getFeatureStatus(merchantId).then((res) => {
  117. if (res.data.code !== 200) return;
  118. const data = res.data.result || {};
  119. this.canAdd = Number(data.auditStatus) === 2 && Number(data.operationStatus) === 1;
  120. });
  121. },
  122. ensureLocation() {
  123. if (this.longitude != null && this.latitude != null) {
  124. return Promise.resolve({
  125. longitude: this.longitude,
  126. latitude: this.latitude
  127. });
  128. }
  129. return getMerchantLocation().then((loc) => {
  130. this.longitude = loc.longitude;
  131. this.latitude = loc.latitude;
  132. return loc;
  133. });
  134. },
  135. loadList() {
  136. this.loading = true;
  137. this.ensureLocation()
  138. .then((loc) =>
  139. prepaidApi.getSharedStores({
  140. pageNo: 1,
  141. pageSize: 100,
  142. longitude: loc.longitude,
  143. latitude: loc.latitude
  144. })
  145. )
  146. .then((res) => {
  147. if (res.data.code !== 200) {
  148. this.storeList = [];
  149. uni.showToast({ title: res.data.message || '加载失败', icon: 'none' });
  150. return;
  151. }
  152. const records = this.parseRecords(res.data.result);
  153. this.storeList = records.map((item) => this.mapStore(item));
  154. })
  155. .catch((err) => {
  156. this.storeList = [];
  157. uni.showToast({
  158. title: (err && err.message) || '加载失败,请稍后重试',
  159. icon: 'none'
  160. });
  161. })
  162. .then(() => {
  163. this.loading = false;
  164. });
  165. },
  166. goAdd() {
  167. if (!this.canAdd) {
  168. uni.showToast({ title: '请先开启储值功能', icon: 'none' });
  169. return;
  170. }
  171. uni.navigateTo({ url: '/pages/topUp/shareStoreAdd' });
  172. },
  173. onDelete(item) {
  174. this.pendingDelete = item;
  175. this.confirmVisible = true;
  176. },
  177. onConfirmCancel() {
  178. this.confirmVisible = false;
  179. this.pendingDelete = null;
  180. },
  181. confirmDelete() {
  182. const item = this.pendingDelete;
  183. this.confirmVisible = false;
  184. this.pendingDelete = null;
  185. if (!item || this.deleting) return;
  186. this.deleting = true;
  187. prepaidApi
  188. .removeSharedStore(item.id)
  189. .then((res) => {
  190. if (res.data.code === 200) {
  191. uni.showToast({ title: '已删除共享', icon: 'none' });
  192. this.loadList();
  193. } else {
  194. uni.showToast({ title: res.data.message || '删除失败', icon: 'none' });
  195. }
  196. })
  197. .catch(() => {
  198. uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  199. })
  200. .then(() => {
  201. this.deleting = false;
  202. });
  203. }
  204. }
  205. };
  206. </script>
  207. <style scoped lang="scss">
  208. .page {
  209. min-height: 100vh;
  210. background: #f7f7f7;
  211. padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
  212. box-sizing: border-box;
  213. }
  214. .list {
  215. padding: 24rpx;
  216. }
  217. .store-card {
  218. position: relative;
  219. display: flex;
  220. align-items: flex-start;
  221. background: #fff;
  222. border-radius: 16rpx;
  223. padding: 24rpx;
  224. margin-bottom: 20rpx;
  225. overflow: hidden;
  226. .del-btn {
  227. position: absolute;
  228. top: 0;
  229. right: 0;
  230. width: 96rpx;
  231. height: 44rpx;
  232. z-index: 2;
  233. .del-bg {
  234. position: absolute;
  235. left: 0;
  236. top: 0;
  237. width: 100%;
  238. height: 100%;
  239. }
  240. .del-text {
  241. position: relative;
  242. z-index: 1;
  243. display: flex;
  244. align-items: center;
  245. justify-content: center;
  246. width: 100%;
  247. height: 100%;
  248. font-size: 22rpx;
  249. color: #1C7CF9;
  250. line-height: 1;
  251. padding-left: 8rpx;
  252. box-sizing: border-box;
  253. }
  254. }
  255. .logo {
  256. width: 120rpx;
  257. height: 120rpx;
  258. border-radius: 12rpx;
  259. background: #f0f0f0;
  260. flex-shrink: 0;
  261. margin-right: 20rpx;
  262. }
  263. .info {
  264. flex: 1;
  265. min-width: 0;
  266. padding-right: 80rpx;
  267. position: relative;
  268. .name {
  269. font-size: 30rpx;
  270. font-weight: 600;
  271. color: #333;
  272. line-height: 1.35;
  273. margin-bottom: 10rpx;
  274. }
  275. .meta {
  276. display: flex;
  277. align-items: center;
  278. gap: 8rpx;
  279. margin-bottom: 10rpx;
  280. .stars {
  281. display: flex;
  282. align-items: center;
  283. gap: 2rpx;
  284. }
  285. .meta-text {
  286. font-size: 22rpx;
  287. color: #FF4D4F;
  288. line-height: 1.2;
  289. }
  290. }
  291. .addr {
  292. font-size: 22rpx;
  293. color: #999;
  294. line-height: 1.4;
  295. padding-right: 120rpx;
  296. }
  297. .distance {
  298. position: absolute;
  299. right: 0;
  300. bottom: 0;
  301. font-size: 22rpx;
  302. color: #666;
  303. line-height: 1.4;
  304. }
  305. }
  306. }
  307. .empty-wrap {
  308. padding-top: 200rpx;
  309. display: flex;
  310. flex-direction: column;
  311. align-items: center;
  312. justify-content: center;
  313. gap: 16rpx;
  314. .empty-text {
  315. font-size: 26rpx;
  316. color: #999;
  317. }
  318. }
  319. .footer {
  320. position: fixed;
  321. left: 0;
  322. right: 0;
  323. bottom: 0;
  324. padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
  325. background: #fff;
  326. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
  327. z-index: 20;
  328. .add-btn {
  329. height: 88rpx;
  330. line-height: 88rpx;
  331. text-align: center;
  332. background: linear-gradient(269deg, #2260F6 0%, #0FB0FF 100%);
  333. border-radius: 160rpx;
  334. color: #fff;
  335. font-size: 32rpx;
  336. font-weight: 500;
  337. &.disabled {
  338. background: #B7D4FF;
  339. color: #fff;
  340. }
  341. }
  342. }
  343. .confirm-dialog {
  344. width: 560rpx;
  345. background: #fff;
  346. border-radius: 24rpx;
  347. overflow: hidden;
  348. padding: 48rpx 40rpx 40rpx;
  349. box-sizing: border-box;
  350. .confirm-title {
  351. text-align: center;
  352. font-size: 34rpx;
  353. font-weight: 600;
  354. color: #333;
  355. line-height: 1.4;
  356. margin-bottom: 28rpx;
  357. }
  358. .confirm-body {
  359. padding: 0 8rpx 40rpx;
  360. .confirm-text {
  361. display: block;
  362. text-align: center;
  363. font-size: 28rpx;
  364. color: #333;
  365. line-height: 1.6;
  366. }
  367. }
  368. .confirm-footer {
  369. display: flex;
  370. align-items: center;
  371. justify-content: space-between;
  372. gap: 24rpx;
  373. .btn {
  374. flex: 1;
  375. height: 80rpx;
  376. line-height: 80rpx;
  377. text-align: center;
  378. border-radius: 40rpx;
  379. font-size: 30rpx;
  380. box-sizing: border-box;
  381. }
  382. .cancel {
  383. color: #999;
  384. background: #fff;
  385. border: 2rpx solid #dcdcdc;
  386. }
  387. .ok {
  388. color: #fff;
  389. background: linear-gradient(90deg, #3ab3ff 0%, #2472ff 100%);
  390. border: none;
  391. }
  392. }
  393. }
  394. </style>