index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <template>
  2. <view class="container" :style="{ background: containerBg }" :class="bookList.length ? 'book-list' : 'no-list'">
  3. <u-navbar :is-back="false" :border-bottom="false" :background="{ background: navbarBackground }">
  4. <text class="nav-title">卖书给书嗨</text>
  5. </u-navbar>
  6. <not-scanned v-if="!bookList.length"></not-scanned>
  7. <scan-book-list v-else @updateBooks="updateBooksList" @deleted="getLastOrder" :bookList="bookList"
  8. ref="scanBookList"></scan-book-list>
  9. <!-- 底部固定按钮 -->
  10. <view class="bottom-fixed">
  11. <view class="btn-wrap mb-20">
  12. <button class="scan-btn flex-1" @click="handleScan">
  13. <u-icon name="scan" color="#FFFFFF" size="40"></u-icon>
  14. <text>扫码卖书</text>
  15. </button>
  16. <button class="isbn-btn flex-1" @click="goToInputISBN">
  17. <u-icon name="edit-pen" color="#4CD964" size="40"></u-icon>
  18. <text>输入ISBN</text>
  19. </button>
  20. </view>
  21. <view class="flex-a flex-j-b pad-20" style="padding-top: 0" v-if="bookList.length">
  22. <view class="left-info" style="min-width: 194px">
  23. <view class="flex-a common-text">
  24. 共<text class="color-red">{{ totalBooks }}</text>件 预估回收价 <text
  25. class="color-red">¥{{ totalPrice || 0 }}</text>
  26. </view>
  27. <text class="common-text tip">*旧书预估价格满 {{ orderInfo.minOrderMoney }} 元起收</text>
  28. </view>
  29. <button class="scan-btn next-btn" @click="onNext" :disabled="isDisabled">下一步</button>
  30. </view>
  31. <service-info v-if="bookList.length" :serviceList="serviceList"
  32. :firstOrder="orderInfo.firstOrder"></service-info>
  33. </view>
  34. <InputIsbn ref="isbnPopup" @submit="checkBookISBN" />
  35. <!-- 套装书说明弹窗 -->
  36. <CommonDialog ref="setBookDialog" title="套装书说明" :showCancel="false" @confirm="handleSetBookConfirm">
  37. <text>套装书(ISBN码相同的系列书箱)只需扫描其中一册,扫码价即套装价。打包时请把所有单册统在一起或放在一个袋子里寄出。</text>
  38. </CommonDialog>
  39. <!-- 暂不回收弹窗 -->
  40. <CommonDialog ref="notAcceptDialog" title="暂不回收" :showCancel="false">
  41. <text>这本书暂时不回收,请您过段时间再来试试~</text>
  42. </CommonDialog>
  43. <!-- 暂无信息弹窗 -->
  44. <CommonDialog ref="noInfoDialog" title="暂无信息" :showCancel="false">
  45. <text>抱歉,没有该书的信息,书嗨会定期补充图书信息,请您过段时间再来试试~</text>
  46. </CommonDialog>
  47. <!-- 扫累了弹窗 -->
  48. <CommonDialog ref="tiredDialog" title="温馨提示" :showCancel="false">
  49. <text>扫累了,休息休息吧~</text>
  50. </CommonDialog>
  51. <!-- 该书超出最大回收本数 maxAcceptDialog-->
  52. <CommonDialog ref="maxAcceptDialog" title="温馨提示" :showCancel="false">
  53. <text>该书超出最大回收本数</text>
  54. </CommonDialog>
  55. <!-- 单个订单最多40本书 orderMaxNumDialog-->
  56. <CommonDialog ref="orderMaxNumDialog" title="温馨提示" :showCancel="false">
  57. <text>单个订单最多40本书</text>
  58. </CommonDialog>
  59. <!-- 温馨提示弹窗 -->
  60. <KindReminder ref="kindReminder" @start="handleStartSelling" @viewRules="handleViewRules" />
  61. <view class="customer-service"
  62. :style="{ left: servicePosition.left + 'px', right: servicePosition.right + 'px', bottom: servicePosition.bottom + 'px' }"
  63. @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
  64. <button class="service-btn" open-type="contact">
  65. <image src="/static/img/kf.png" mode="widthFix" style="width: 126rpx; height: 140rpx"></image>
  66. </button>
  67. </view>
  68. <ConfirmBooks ref="confirmBooks" @incomplete="handleIncomplete" />
  69. <!-- 首单免费弹窗 -->
  70. <FirstOrderFreePopup ref="firstOrderFreePopup" />
  71. </view>
  72. </template>
  73. <script>
  74. import notScanned from "./components/notScanned.vue";
  75. import InputIsbn from "./components/InputIsbn.vue";
  76. import ScanBookList from "./components/ScanBookList.vue";
  77. import CommonDialog from "@/components/common-dialog.vue";
  78. import KindReminder from "./components/KindReminder.vue";
  79. import ServiceInfo from "./components/ServiceInfo.vue";
  80. import ConfirmBooks from "./components/ConfirmBooks.vue";
  81. import FirstOrderFreePopup from "./components/FirstOrderFreePopup.vue";
  82. export default {
  83. components: {
  84. notScanned,
  85. InputIsbn,
  86. ScanBookList,
  87. CommonDialog,
  88. KindReminder,
  89. ServiceInfo,
  90. ConfirmBooks,
  91. FirstOrderFreePopup,
  92. },
  93. data() {
  94. return {
  95. orderInfo: {},
  96. collapseState: {
  97. step1: false,
  98. step3: false,
  99. },
  100. scrollTop: 0,
  101. bookList: [],
  102. serviceList: [],
  103. currentBook: {},
  104. // 客服按钮位置
  105. servicePosition: {
  106. left: 'auto',
  107. right: 0,
  108. bottom: '20%'
  109. },
  110. // 触摸开始位置
  111. startX: 0,
  112. startY: 0,
  113. // 屏幕宽度和高度
  114. screenWidth: 0,
  115. screenHeight: 0,
  116. // 初始位置记录,用于计算拖动
  117. initialLeft: 0,
  118. initialBottom: 0,
  119. // 是否正在更新位置,用于防止频繁更新
  120. isUpdatingPosition: false,
  121. };
  122. },
  123. computed: {
  124. navbarBackground() {
  125. if (this.scrollTop > 0) {
  126. return "linear-gradient(180deg, #4CD964 0%, #5ff178 100%)";
  127. }
  128. return "transparent";
  129. },
  130. containerBg() {
  131. return this.bookList.length > 0 ?
  132. "linear-gradient(180deg, #4CD964 0%, #F8F8F8 25%)" :
  133. "linear-gradient(180deg, #4CD964 0%, #ffffff 25%)";
  134. },
  135. containerPb() {
  136. return this.bookList.length > 0 ? "300rpx" : "110rpx";
  137. },
  138. totalBooks() {
  139. return this.bookList.reduce((sum, book) => sum + (book.num || 1), 0);
  140. },
  141. totalPrice() {
  142. return this.bookList.reduce((sum, book) => sum + book.recyclePrice * (book.num || 1), 0).toFixed(2);
  143. },
  144. isDisabled() {
  145. return this.totalPrice < this.orderInfo.minOrderMoney;
  146. },
  147. },
  148. onPageScroll(e) {
  149. this.$nextTick(() => {
  150. this.scrollTop = e.scrollTop;
  151. });
  152. },
  153. onReady() {
  154. // 获取屏幕宽度和高度
  155. uni.getSystemInfo({
  156. success: (res) => {
  157. this.screenWidth = res.windowWidth;
  158. this.screenHeight = res.windowHeight;
  159. }
  160. });
  161. },
  162. methods: {
  163. slientLogin() {
  164. let inviteCode = wx.getStorageSync('inviteCode') || ''
  165. let token = wx.getStorageSync('token')
  166. if (token) return;
  167. uni.login({
  168. success(res) {
  169. uni.$u.http
  170. .post("/user/wxLogin", {
  171. code: res.code,
  172. inviteCode,
  173. })
  174. .then((response) => {
  175. if (response.code == 200) {
  176. uni.setStorageSync("token", response.data.token);
  177. }
  178. });
  179. },
  180. fail: (err) => {
  181. console.log(err, "wx.login登录失败");
  182. },
  183. });
  184. },
  185. // 触摸开始
  186. touchStart(e) {
  187. const touch = e.touches[0];
  188. this.startX = touch.clientX;
  189. this.startY = touch.clientY;
  190. // 记录初始位置,用于计算移动距离
  191. if (this.servicePosition.right !== 'auto') {
  192. // 如果是靠右定位,记录当前位置但不立即改变显示位置
  193. // 只在内部计算中使用,避免视觉上的位置跳动
  194. this.initialLeft = this.screenWidth - 126;
  195. } else {
  196. this.initialLeft = parseFloat(this.servicePosition.left);
  197. }
  198. // 如果bottom是百分比,转换为具体像素值,但不改变显示位置
  199. if (typeof this.servicePosition.bottom === 'string' && this.servicePosition.bottom.includes('%')) {
  200. const percentage = parseFloat(this.servicePosition.bottom) / 100;
  201. this.initialBottom = this.screenHeight * percentage;
  202. } else {
  203. this.initialBottom = parseFloat(this.servicePosition.bottom);
  204. }
  205. },
  206. // 触摸移动
  207. touchMove(e) {
  208. // 阻止默认行为,防止页面滚动
  209. e.preventDefault && e.preventDefault();
  210. e.stopPropagation && e.stopPropagation();
  211. const touch = e.touches[0];
  212. // 计算移动距离
  213. const deltaX = touch.clientX - this.startX;
  214. const deltaY = touch.clientY - this.startY;
  215. // 使用初始位置计算新位置,避免累积误差
  216. let newLeft = this.initialLeft + deltaX;
  217. let newBottom = this.initialBottom - deltaY; // 注意:y轴方向是相反的
  218. // 确保按钮不超出屏幕边界
  219. if (newLeft < 0) {
  220. newLeft = 0;
  221. } else if (newLeft > this.screenWidth - 126) {
  222. newLeft = this.screenWidth - 126;
  223. }
  224. // 确保按钮不超出屏幕垂直边界
  225. if (newBottom < 20) {
  226. newBottom = 20;
  227. } else if (newBottom > this.screenHeight - 160) {
  228. newBottom = this.screenHeight - 160;
  229. }
  230. // 使用节流方式更新位置,避免过于频繁的更新
  231. if (!this.isUpdatingPosition) {
  232. this.isUpdatingPosition = true;
  233. // 更新位置 - 第一次移动时才真正改变right为auto
  234. this.servicePosition = {
  235. left: newLeft,
  236. right: 'auto',
  237. bottom: newBottom
  238. };
  239. // 使用setTimeout代替requestAnimationFrame,在微信小程序中更兼容
  240. setTimeout(() => {
  241. this.isUpdatingPosition = false;
  242. }, 16); // 约等于60fps的刷新率
  243. }
  244. // 不更新起始点,保持相对于初始触摸点的位移计算
  245. // 这样可以避免累积误差,使拖动更精确
  246. },
  247. // 触摸结束,实现吸附效果
  248. touchEnd() {
  249. // 确保不再有待处理的更新
  250. this.isUpdatingPosition = false;
  251. const buttonCenter = this.servicePosition.left + 63; // 按钮中心位置
  252. const halfScreen = this.screenWidth / 2;
  253. // 判断是吸附到左边还是右边
  254. if (buttonCenter < halfScreen) {
  255. // 吸附到左边
  256. this.servicePosition = {
  257. left: 0,
  258. right: 'auto',
  259. bottom: this.servicePosition.bottom
  260. };
  261. } else {
  262. // 吸附到右边
  263. this.servicePosition = {
  264. left: 'auto',
  265. right: 0,
  266. bottom: this.servicePosition.bottom
  267. };
  268. }
  269. },
  270. handleStart() {
  271. this.showPopup = true;
  272. },
  273. //套装书确认
  274. handleSetBookConfirm() {
  275. this.$refs.confirmBooks.openPopup(this.currentBook);
  276. },
  277. //书册补全
  278. handleIncomplete() {
  279. this.$refs.scanBookList.handleDeleteBook(this.currentBook);
  280. },
  281. //提交
  282. onNext() {
  283. let orderId = this.orderInfo.orderId;
  284. //预提交
  285. uni.$u.http.get("/token/order/preSubmit?orderId=" + orderId).then((res) => {
  286. if (res.code == 200) {
  287. if (res.data.code == 1 || res.data.code == 2) {
  288. uni.navigateTo({
  289. url: "/pages-home/pages/book-order",
  290. });
  291. uni.setStorageSync("orderId", orderId);
  292. } else {
  293. uni.showToast({
  294. icon: "none",
  295. title: res.msg,
  296. });
  297. }
  298. } else {
  299. uni.showToast({
  300. icon: "none",
  301. title: res.msg,
  302. });
  303. }
  304. });
  305. },
  306. updateBooksList(data) {
  307. this.bookList = data;
  308. },
  309. toggleCollapse(step) {
  310. this.$set(this.collapseState, step, !this.collapseState[step]);
  311. },
  312. handleScan() {
  313. uni.scanCode({
  314. scanType: ["barCode"],
  315. success: (res) => {
  316. this.checkBookISBN(res.result);
  317. },
  318. fail: () => {
  319. uni.showToast({
  320. title: "扫码失败",
  321. icon: "none",
  322. });
  323. },
  324. });
  325. },
  326. checkBookISBN(isbn) {
  327. uni.$u.http.get("/token/order/scanIsbn?isbn=" + isbn).then((res) => {
  328. if (res.code == 200) {
  329. let code = res.data.code;
  330. if (code == 1) {
  331. res.data.num = 1;
  332. res.data.status = 1;
  333. res.data.recyclePrice = res.data.recycleMoney;
  334. this.currentBook = res.data;
  335. this.bookList.push(res.data);
  336. if (res.data.suit == 1) {
  337. this.$refs.setBookDialog.openPopup();
  338. }
  339. } else if (code == 2) {
  340. let item = this.bookList.find((v) => v.isbn === isbn);
  341. item.num = item.num + 1;
  342. } else {
  343. this.handleBookCode(res.data.code);
  344. }
  345. } else {
  346. uni.showToast({
  347. title: res.msg,
  348. icon: "none",
  349. });
  350. }
  351. });
  352. },
  353. //处理扫码之后不同的状态 0-扫码频繁 1-成功 2-本单已有该书,数量+1 3-没有该书 4-本书暂不回收 5-超过每单最大可卖数量 6-单个订单最多40本书
  354. handleBookCode(code) {
  355. if (code == 1) {
  356. this.bookList.push();
  357. }
  358. let tempKeys = [
  359. "tiredDialog",
  360. "",
  361. "",
  362. "noInfoDialog",
  363. "notAcceptDialog",
  364. "maxAcceptDialog",
  365. "orderMaxNumDialog",
  366. ];
  367. let key = tempKeys[code];
  368. if (key) {
  369. this.$refs[key].openPopup();
  370. }
  371. },
  372. //获取当前用户未提交订单 /api/token/order/lastOrder
  373. getLastOrder() {
  374. uni.$u.http.get("/token/order/lastOrder").then((res) => {
  375. if (res.code == 200) {
  376. this.orderInfo = res.data;
  377. if (res.data.showDialog == 1) {
  378. this.$refs.firstOrderFreePopup.openPopup();
  379. } else if (res.data.showDialog == 2) {
  380. this.$refs.kindReminder.openPopup();
  381. }
  382. this.bookList = res.data?.orderDetailList ?
  383. res.data.orderDetailList.map((v) => {
  384. v.orderId = res.data.orderId;
  385. return v;
  386. }) : [];
  387. this.serviceList = res.data.serviceList || [];
  388. }
  389. });
  390. },
  391. goToScannedBooks() {
  392. uni.navigateTo({
  393. url: "/pages-home/pages/scaned-book",
  394. });
  395. },
  396. goToInputISBN() {
  397. this.$refs.isbnPopup.openPopup();
  398. },
  399. handleStartSelling() {
  400. // 标记已显示过温馨提示
  401. uni.setStorageSync("kindReminderShown", true);
  402. },
  403. },
  404. onShow() {
  405. // 获取上一个订单
  406. setTimeout(() => {
  407. this.getLastOrder();
  408. setTimeout(() => {
  409. this.slientLogin()
  410. }, 10)
  411. }, 300);
  412. },
  413. };
  414. </script>
  415. <style lang="scss" scoped>
  416. .customer-service {
  417. position: fixed;
  418. width: 126rpx;
  419. height: 140rpx;
  420. bottom: 20%;
  421. z-index: 999;
  422. transition: all 0.3s ease;
  423. /* 添加过渡效果使吸附更平滑 */
  424. button {
  425. height: max-content;
  426. background-color: transparent;
  427. padding: 0;
  428. }
  429. }
  430. .container {
  431. height: 100%;
  432. position: relative;
  433. overflow: auto;
  434. z-index: 1;
  435. /* #ifdef MP-WEIXIN */
  436. min-height: 100vh;
  437. /* #endif */
  438. /* #ifndef MP-WEIXIN */
  439. min-height: calc(100vh - 120rpx);
  440. /* #endif */
  441. &.book-list {
  442. padding-bottom: 300rpx;
  443. }
  444. padding-bottom: 130rpx;
  445. .nav-title {
  446. font-family: PingFang SC;
  447. font-weight: bold;
  448. font-size: 34rpx;
  449. color: #ffffff;
  450. padding-left: 40rpx;
  451. }
  452. }
  453. .common-text {
  454. font-family: PingFang SC;
  455. font-weight: 500;
  456. font-size: 28rpx;
  457. color: #999999;
  458. &.tip {
  459. color: #ff8a4b;
  460. }
  461. }
  462. .color-red {
  463. color: #ff0000;
  464. margin: 0 10rpx;
  465. }
  466. .bottom-fixed {
  467. position: fixed;
  468. left: 0;
  469. right: 0;
  470. bottom: 0;
  471. z-index: 9;
  472. background-color: #ffffff;
  473. /* #ifdef H5 */
  474. padding-bottom: 120rpx;
  475. /* #endif */
  476. .btn-wrap {
  477. display: flex;
  478. gap: 20rpx;
  479. padding: 20rpx;
  480. padding-bottom: 0;
  481. button {
  482. flex: 1;
  483. height: 88rpx;
  484. border-radius: 10rpx;
  485. display: flex;
  486. align-items: center;
  487. justify-content: center;
  488. gap: 10rpx;
  489. border: none;
  490. text {
  491. font-size: 32rpx;
  492. }
  493. &::after {
  494. border: none;
  495. }
  496. }
  497. .isbn-btn {
  498. background-color: #ffffff;
  499. color: #4cd964;
  500. border: 3rpx solid #4cd964;
  501. }
  502. }
  503. .scan-btn {
  504. background-color: #4cd964;
  505. color: #ffffff;
  506. }
  507. .next-btn {
  508. margin: 0;
  509. margin-left: 20rpx;
  510. &[aria-disabled="true"] {
  511. background-color: #cccccc;
  512. color: #ffffff;
  513. opacity: 0.7;
  514. cursor: not-allowed;
  515. }
  516. }
  517. }
  518. </style>