index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <template>
  2. <view class="nav" :style="{ paddingTop: statusBarHeight + 'px' }">
  3. <text
  4. class="nav-back"
  5. :style="{ top: statusBarHeight + 'px' }"
  6. @click="onBack"
  7. ></text>
  8. <view class="nav-title">客户扫码率</view>
  9. </view>
  10. <view class="page" @click="closeDropdown">
  11. <view
  12. class="header-card"
  13. :style="{ paddingTop: statusBarHeight + 60 + 'px' }"
  14. >
  15. <view class="meta">
  16. <view class="dot"></view>
  17. <text class="meta-text"
  18. >等级:{{
  19. scanType == 1 ? "VIP" : scanType == 2 ? "二级" : "三级"
  20. }}客户</text
  21. >
  22. </view>
  23. <view class="meta">
  24. <view class="dot"></view>
  25. <text class="meta-text">片区:{{ regionName }}</text>
  26. </view>
  27. <view class="meta">
  28. <view class="dot"></view>
  29. <text class="meta-text">品种:{{ physicName }}</text>
  30. </view>
  31. <view class="meta">
  32. <view class="dot"></view>
  33. <text class="meta-text">时间区间:{{ dateRange }}</text>
  34. </view>
  35. <view class="selector-wrap" @click.stop>
  36. <input
  37. class="selector-input"
  38. v-model="customerInput.customerName"
  39. placeholder="选择客户"
  40. @focus="openDropdown"
  41. @input="onInputChange"
  42. />
  43. <view
  44. class="selector-arrow"
  45. :class="{ open: dropdownOpen }"
  46. @click="toggleDropdown"
  47. ></view>
  48. <view class="dropdown" v-show="dropdownOpen" @click.stop>
  49. <scroll-view
  50. scroll-y
  51. :style="{ maxHeight: '300rpx' }"
  52. lower-threshold="20"
  53. @scrolltolower="getCustomerInfo"
  54. >
  55. <view
  56. class="dropdown-item"
  57. v-for="(c, i) in customers"
  58. :key="i"
  59. @click="selectCustomer(c)"
  60. >{{ c.customerName || "--" }}</view
  61. >
  62. <view v-if="customerHasMore" class="customer-loading-wrap">
  63. <image
  64. src="../../../../static/images/loading.png"
  65. mode="scaleToFill"
  66. class="loading-icon"
  67. />
  68. </view>
  69. </scroll-view>
  70. </view>
  71. </view>
  72. </view>
  73. <view class="card">
  74. <ScanRateTable
  75. :api="`/bills/getScanRateDetail`"
  76. :columns="columns"
  77. :tableBodyHeight="tableBodyHeight"
  78. :showHeader="false"
  79. bodyFontColor="#000"
  80. summaryBold="true"
  81. tableType="customerDetail"
  82. :params="queryParams"
  83. />
  84. </view>
  85. </view>
  86. </template>
  87. <script>
  88. import ScanRateTable from "../wigets/ScanRateTable.vue";
  89. import request from '../../../../request/index.js'
  90. export default {
  91. components: {
  92. ScanRateTable,
  93. },
  94. data() {
  95. return {
  96. customerLevel: "",
  97. rangeType: "",
  98. regionCode: "",
  99. drugEntBaseInfoId: "",
  100. scanType: "",
  101. statusBarHeight: 20,
  102. regionName: "",
  103. physicName: "",
  104. customerPage: 1,
  105. customerPageSize: 10,
  106. customerTotal: null,
  107. customerHasMore: true,
  108. customerLoading: false,
  109. customers: [],
  110. customerInput: "",
  111. customerDebounceTimer: null,
  112. selectedCustomer: "",
  113. dropdownOpen: false,
  114. totalCount: 30,
  115. isLoading: false,
  116. list: [],
  117. columns: [
  118. {
  119. key: "custemerName",
  120. title: "客户名称",
  121. underline: true,
  122. fixed: false,
  123. width: "",
  124. },
  125. {
  126. key: "outScanRate",
  127. title: "出库扫码率",
  128. fixed: false,
  129. width: "",
  130. },
  131. {
  132. key: "inScanRate",
  133. title: "入库扫码率",
  134. fixed: false,
  135. width: "",
  136. },
  137. {
  138. key: "selfExaminationRate",
  139. title: "自验证率",
  140. fixed: false,
  141. width: "",
  142. },
  143. ],
  144. };
  145. },
  146. onLoad(options) {
  147. const info = uni.getSystemInfoSync();
  148. this.statusBarHeight = info.statusBarHeight || 20;
  149. this.list = this.genRows(Math.min(10, this.totalCount));
  150. this.rangeType = options.type || 1;
  151. this.regionCode = options.regionCode || "";
  152. this.drugEntBaseInfoId = options.drugEntBaseInfoId || "";
  153. this.scanType = options.scanType || "1";
  154. this.physicName = decodeURIComponent(options.physicName || "--");
  155. this.regionName = decodeURIComponent(options.regionName || "--");
  156. this.customerLevel =
  157. this.scanType == 1 ? "VIP" : this.scanType == 2 ? "二级" : "三级";
  158. this.getCustomerInfo();
  159. },
  160. watch: {
  161. customerInput: {
  162. handler(newVal, oldVal) {
  163. if (
  164. !newVal?.customerName ||
  165. newVal?.customerName !== oldVal?.customerName
  166. ) {
  167. if (this.customerDebounceTimer)
  168. clearTimeout(this.customerDebounceTimer);
  169. this.customerDebounceTimer = setTimeout(() => {
  170. this.customerPage = 1;
  171. this.customers = [];
  172. this.customerHasMore = true;
  173. this.getCustomerInfo();
  174. }, 500);
  175. }
  176. },
  177. deep: true,
  178. },
  179. },
  180. computed: {
  181. dateRange() {
  182. const t = Number(this.rangeType) || 1;
  183. const now = new Date();
  184. const pad = (n) => (n < 10 ? "0" + n : "" + n);
  185. const fmt = (d) =>
  186. `${d.getFullYear()}.${pad(d.getMonth() + 1)}.${pad(d.getDate())}`;
  187. let start;
  188. let end;
  189. if (t === 2) {
  190. const y = now.getFullYear();
  191. const m = now.getMonth();
  192. start = new Date(y, m, 1);
  193. end = new Date(y, m + 1, 0);
  194. } else if (t === 3) {
  195. const y = now.getFullYear();
  196. const m = now.getMonth();
  197. start = new Date(y, m - 1, 1);
  198. end = new Date(y, m, 0);
  199. } else if (t === 4) {
  200. end = now;
  201. start = new Date(now.getTime() - 59 * 24 * 60 * 60 * 1000);
  202. } else {
  203. end = now;
  204. start = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000);
  205. }
  206. return `${fmt(start)}-${fmt(end)}`;
  207. },
  208. queryParams() {
  209. return {
  210. type: this.rangeType,
  211. regionCode: this.regionCode,
  212. drugEntBaseInfoId: this.drugEntBaseInfoId,
  213. customerName: this.customerInput?.customerName,
  214. scanType: this.scanType,
  215. };
  216. },
  217. tableBodyHeight() {
  218. const rowHeight = 76; // rpx
  219. if (this.totalCount >= 9) return `${rowHeight * 8.5}rpx`;
  220. return "auto";
  221. },
  222. },
  223. methods: {
  224. getCustomerInfo() {
  225. if (!this.customerHasMore || this.customerLoading) return;
  226. this.customerLoading = true;
  227. request('/customer/info/getCustomerInfo', {
  228. tracCode: traceCode,
  229. path: '/traceabilityReport/pages/customerScanningRate/detail/index.vue',
  230. }).then(res => {
  231. if (res.code == 200) {
  232. const _data = res.data || {};
  233. this.customerTotal = _data.total;
  234. // if (this.customerTotal <= this.customers.length) {
  235. // this.customerHasMore = false;
  236. // this.customerLoading = false;
  237. // return;
  238. // } else {
  239. const list = Array.isArray(_data.list) ? _data.list : [];
  240. this.customers = [...this.customers, ..._list];
  241. if (this.customerTotal <= this.customers.length) {
  242. this.customerHasMore = false;
  243. } else {
  244. this.customerPage++;
  245. this.customerHasMore = true;
  246. }
  247. // }
  248. this.customerLoading = false;
  249. }
  250. })
  251. },
  252. openDropdown() {
  253. this.dropdownOpen = true;
  254. },
  255. toggleDropdown() {
  256. this.dropdownOpen = !this.dropdownOpen;
  257. },
  258. onInputChange(e) {
  259. const v = e?.detail?.value ?? this.customerInput;
  260. this.customerInput.customerName = v;
  261. },
  262. selectCustomer(c) {
  263. this.selectedCustomer = c;
  264. this.customerInput = c;
  265. this.dropdownOpen = false;
  266. },
  267. closeDropdown() {
  268. this.dropdownOpen = false;
  269. },
  270. onBack() {
  271. try {
  272. uni.navigateBack();
  273. } catch (e) {}
  274. },
  275. onTableScrollToLower() {
  276. if (this.isLoading) return;
  277. if (this.list.length >= this.totalCount) return;
  278. this.isLoading = true;
  279. const remain = this.totalCount - this.list.length;
  280. const toAdd = Math.min(10, remain);
  281. setTimeout(() => {
  282. const more = this.genRows(toAdd);
  283. this.list = this.list.concat(more);
  284. this.isLoading = false;
  285. }, 800);
  286. },
  287. genRows(n) {
  288. const res = [];
  289. const regions = [
  290. "山东医药",
  291. "江苏医药",
  292. "浙江医药",
  293. "广东医药",
  294. "湖南医药",
  295. "湖北医药",
  296. ];
  297. const base = this.list.length;
  298. for (let i = 0; i < n; i++) {
  299. const name = regions[(base + i) % regions.length];
  300. res.push({ region: name, outRate: "0%", inRate: "0%" });
  301. }
  302. return res;
  303. },
  304. },
  305. };
  306. </script>
  307. <style scoped>
  308. .nav {
  309. position: fixed;
  310. top: 0;
  311. left: 0;
  312. right: 0;
  313. height: 44px;
  314. background-color: #2c69ff;
  315. display: flex;
  316. align-items: center;
  317. justify-content: center;
  318. }
  319. .nav-title {
  320. font-size: 36rpx;
  321. color: #fff;
  322. font-weight: 700;
  323. }
  324. .nav-back {
  325. position: absolute;
  326. left: 40rpx;
  327. top: 12rpx;
  328. width: 20rpx;
  329. height: 20rpx;
  330. color: #fff;
  331. border-left: 3rpx solid #fff;
  332. border-bottom: 3rpx solid #fff;
  333. transform: rotate(45deg) translateY(56rpx);
  334. margin-left: 40rpx;
  335. }
  336. .page {
  337. height: 100vh;
  338. }
  339. .card {
  340. margin: 24rpx;
  341. background: #fff;
  342. border-radius: 16rpx;
  343. font-size: 32rpx;
  344. }
  345. /* .table-header {
  346. display: flex;
  347. background: #eaf2ff;
  348. font-weight: bold;
  349. color: #2c69ff;
  350. border-top-left-radius: 16rpx;
  351. border-top-right-radius: 16rpx;
  352. }
  353. .table-body {
  354. margin-top: 8rpx;
  355. }
  356. .row {
  357. display: flex;
  358. align-items: center;
  359. }
  360. .row:last-child {
  361. border-bottom: none;
  362. }
  363. .cell {
  364. flex: 1;
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. height: 76rpx;
  369. } */
  370. .loading-row {
  371. justify-content: center;
  372. }
  373. .loading-wrapper {
  374. width: 100%;
  375. height: 76rpx;
  376. display: flex;
  377. align-items: center;
  378. justify-content: center;
  379. }
  380. .loading-icon {
  381. width: 40rpx;
  382. height: 40rpx;
  383. animation: spin 1s linear infinite;
  384. }
  385. @keyframes spin {
  386. from {
  387. transform: rotate(0deg);
  388. }
  389. to {
  390. transform: rotate(360deg);
  391. }
  392. }
  393. /* .total-row {
  394. font-weight: bold;
  395. color: #2c69ff;
  396. } */
  397. .header-card {
  398. background: #2c69ff;
  399. border-bottom-left-radius: 32rpx;
  400. border-bottom-right-radius: 32rpx;
  401. padding: 36rpx;
  402. color: #fff;
  403. }
  404. .meta {
  405. display: flex;
  406. align-items: center;
  407. margin: 14rpx 0;
  408. }
  409. .dot {
  410. width: 8rpx;
  411. height: 8rpx;
  412. border-radius: 50%;
  413. background: #fff;
  414. margin-right: 20rpx;
  415. }
  416. .meta-text {
  417. font-size: 32rpx;
  418. }
  419. .selector-wrap {
  420. position: relative;
  421. margin-top: 16rpx;
  422. }
  423. .selector-input {
  424. width: 100%;
  425. height: 64rpx;
  426. border-radius: 32rpx;
  427. background: #fff;
  428. color: #333;
  429. padding: 0 72rpx 0 30rpx;
  430. box-sizing: border-box;
  431. font-size: 28rpx;
  432. }
  433. .selector-arrow {
  434. position: absolute;
  435. right: 24rpx;
  436. bottom: 25rpx;
  437. width: 16rpx;
  438. height: 16rpx;
  439. border: 5rpx solid #2c69ff;
  440. border-top: none;
  441. border-left: none;
  442. transform-origin: 50% 50%;
  443. transform: rotate(45deg);
  444. transition: transform 0.2s;
  445. }
  446. .selector-arrow.open {
  447. bottom: 18rpx;
  448. transform: rotate(225deg);
  449. }
  450. .dropdown {
  451. position: absolute;
  452. left: 0;
  453. right: 0;
  454. top: 70rpx;
  455. background: #fff;
  456. border: 1rpx solid #eef0f4;
  457. border-radius: 12rpx;
  458. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
  459. z-index: 10;
  460. max-height: none;
  461. }
  462. .dropdown-item {
  463. padding: 16rpx 24rpx;
  464. font-size: 28rpx;
  465. color: #333;
  466. }
  467. .dropdown-item + .dropdown-item {
  468. border-top: 1rpx solid #f0f2f5;
  469. }
  470. .customer-loading-wrap {
  471. width: 100%;
  472. height: 70rpx;
  473. display: flex;
  474. align-items: center;
  475. justify-content: center;
  476. }
  477. </style>