shareStore.vue 9.9 KB

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