index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <template>
  2. <Water></Water>
  3. <view class="nav" :style="{ paddingTop: statusBarHeight + 'px' }" @click.stop="closeDropdowns">
  4. <text class="nav-back" :style="{ top: statusBarHeight + 'px' }" @click="onBack"></text>
  5. <view class="nav-title">客户扫码率</view>
  6. </view>
  7. <view class="page" @click.stop="closeDropdowns">
  8. <view class="header-card" :style="{ paddingTop: statusBarHeight + 60 + 'px' }">
  9. <view class="meta">
  10. <view class="dot"></view>
  11. <text class="meta-text">等级:{{ scanType == 1 ? "VIP" : scanType == 2 ? "二级" : "三级" }}客户</text>
  12. </view>
  13. <view class="meta">
  14. <view class="dot"></view>
  15. <text class="meta-text">片区:{{ regionName }}</text>
  16. </view>
  17. <view class="meta">
  18. <view class="dot"></view>
  19. <text class="meta-text">品种:{{ physicName || "全品种" }}</text>
  20. </view>
  21. <view class="meta">
  22. <view class="dot"></view>
  23. <text class="meta-text">规格:{{ spkSpec || "全规格" }}</text>
  24. </view>
  25. <view class="meta">
  26. <view class="dot"></view>
  27. <text class="meta-text">厂家:{{ produceEntName || "全厂家" }}</text>
  28. </view>
  29. <view class="meta">
  30. <view class="dot"></view>
  31. <text class="meta-text">时间区间:{{ dateRange }}</text>
  32. </view>
  33. <view class="selector-wrap" @click.stop>
  34. <input class="selector-input" v-model="customerInput.customerName" placeholder="选择客户" @focus="openDropdown" @input="onInputChange" />
  35. <view class="selector-arrow" :class="{ open: dropdownOpen }" @click="toggleDropdown"></view>
  36. <view class="dropdown" v-show="dropdownOpen" @click.stop>
  37. <scroll-view
  38. v-if="customers.length && !customerLoading"
  39. scroll-y
  40. :style="{ maxHeight: '300rpx' }"
  41. lower-threshold="20"
  42. @scrolltolower="getCustomerInfo">
  43. <view class="dropdown-item" v-for="(c, i) in customers" :key="i" @click="selectCustomer(c)">{{ c.customerName || "--" }}</view>
  44. <view v-if="customerHasMore" class="customer-loading-wrap">
  45. <image src="../../../../static/images/loading.png" mode="scaleToFill" class="loading-icon" />
  46. </view>
  47. </scroll-view>
  48. <view v-else-if="customerLoading" class="customer-loading-wrap">
  49. <image src="../../../../static/images/loading.png" mode="scaleToFill" class="loading-icon" />
  50. </view>
  51. <view v-else class="dropdown-item">暂无数据</view>
  52. </view>
  53. </view>
  54. </view>
  55. <view class="card">
  56. <ScanRateTable
  57. ref="scanRateTable"
  58. :api="`/bills/getScanRateDetail`"
  59. :columns="columns"
  60. :tableBodyHeight="tableBodyHeight"
  61. :pageSize="20"
  62. title="筛选条件"
  63. :showHeader="true"
  64. summaryBold="true"
  65. tableType="customerDetail"
  66. :params="queryParams"
  67. :summaryFontColor="'#2c69ff'"
  68. :products="products"
  69. :regions="regions"
  70. :entNames="entNames"
  71. :product="physicName"
  72. :drugEntBaseInfoId="drugEntBaseInfoId"
  73. :spec="spkSpec"
  74. :entName="produceEntName"
  75. :regionCode="regionCode"
  76. :regionName="regionName"
  77. @dropdown-open="closeDropdown"
  78. @filter-change="onTableFilterChange"
  79. :showAreaSearch="false" />
  80. </view>
  81. </view>
  82. </template>
  83. <script>
  84. import ScanRateTable from "../wigets/ScanRateTable.vue";
  85. import Water from "@/components/water/water.vue";
  86. import request from "../../../../request/index.js";
  87. export default {
  88. components: {
  89. ScanRateTable,
  90. Water,
  91. },
  92. data() {
  93. return {
  94. customerLevel: "",
  95. rangeType: "",
  96. regionCode: "",
  97. drugEntBaseInfoId: "",
  98. scanType: "",
  99. statusBarHeight: 20,
  100. regionName: "",
  101. physicName: "",
  102. spkSpec: "",
  103. produceEntName: "",
  104. customerPage: 1,
  105. customerPageSize: 20,
  106. customerTotal: null,
  107. customerHasMore: true,
  108. customerLoading: false,
  109. customers: [],
  110. customerInput: { customerName: "" },
  111. customerDebounceTimer: null,
  112. selectedCustomer: "",
  113. dropdownOpen: false,
  114. totalCount: 30,
  115. isLoading: false,
  116. list: [],
  117. windowHeight: 0,
  118. columns: [
  119. {
  120. key: "custemerName",
  121. title: "客户名称",
  122. underline: false,
  123. fixed: false,
  124. width: "",
  125. },
  126. {
  127. key: "inScanRate",
  128. title: "入库扫码率",
  129. fixed: false,
  130. width: "",
  131. unit: "%",
  132. },
  133. {
  134. key: "outScanRate",
  135. title: "出库扫码率",
  136. fixed: false,
  137. width: "",
  138. unit: "%",
  139. },
  140. // {
  141. // key: "selfExaminationRate",
  142. // title: "自验证率",
  143. // fixed: false,
  144. // width: "",
  145. // unit: "%",
  146. // },
  147. ],
  148. products: [],
  149. regions: [],
  150. entNames: [],
  151. };
  152. },
  153. created() {
  154. Promise.all([this.getEntNameList(), this.getProviceList(), this.getDrugInfoName()]).then(() => {});
  155. },
  156. onLoad(options) {
  157. const info = uni.getSystemInfoSync();
  158. this.statusBarHeight = info.statusBarHeight || 20;
  159. this.windowHeight = info.windowHeight;
  160. // this.list = this.genRows(Math.min(10, this.totalCount));
  161. this.rangeType = options.type || 1;
  162. this.regionCode = options.regionCode || "";
  163. this.drugEntBaseInfoId = this.safeDecode(options.drugEntBaseInfoId || "");
  164. this.scanType = options.scanType || "1";
  165. this.physicName = this.safeDecode(options.physicName || "");
  166. this.regionName = this.safeDecode(options.regionName || "");
  167. this.spkSpec = this.safeDecode(options.spkSpec || "");
  168. this.produceEntName = this.safeDecode(options.produceEntName || "");
  169. this.customerLevel = this.scanType == 1 ? "VIP" : this.scanType == 2 ? "二级" : "三级";
  170. this.getCustomerInfo();
  171. },
  172. onShareAppMessage() {
  173. return {
  174. title: "客户扫码率详情",
  175. path: this.buildSharePath(),
  176. };
  177. },
  178. onShareTimeline() {
  179. return {
  180. title: "客户扫码率详情",
  181. query: this.buildShareQuery(),
  182. };
  183. },
  184. watch: {
  185. "customerInput.customerName": {
  186. handler(newVal, oldVal) {
  187. if (!newVal || newVal !== oldVal) {
  188. if (this.customerDebounceTimer) clearTimeout(this.customerDebounceTimer);
  189. this.customerDebounceTimer = setTimeout(() => {
  190. this.customerPage = 1;
  191. this.customers = [];
  192. this.customerHasMore = true;
  193. this.getCustomerInfo();
  194. }, 500);
  195. }
  196. },
  197. deep: true,
  198. },
  199. },
  200. computed: {
  201. dateRange() {
  202. const t = Number(this.rangeType) || 1;
  203. const now = new Date();
  204. const pad = (n) => (n < 10 ? "0" + n : "" + n);
  205. const fmt = (d) => `${d.getFullYear()}.${pad(d.getMonth() + 1)}.${pad(d.getDate())}`;
  206. let start;
  207. let end;
  208. if (t === 2) {
  209. const y = now.getFullYear();
  210. const m = now.getMonth();
  211. start = new Date(y, m, 1);
  212. end = new Date(y, m + 1, 0);
  213. } else if (t === 3) {
  214. const y = now.getFullYear();
  215. const m = now.getMonth();
  216. start = new Date(y, m - 1, 1);
  217. end = new Date(y, m, 0);
  218. } else if (t === 4) {
  219. end = now;
  220. start = new Date(now.getTime() - 59 * 24 * 60 * 60 * 1000);
  221. } else {
  222. end = now;
  223. start = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000);
  224. }
  225. return `${fmt(start)}-${fmt(end)}`;
  226. },
  227. queryParams() {
  228. return {
  229. type: this.rangeType,
  230. regionCode: this.regionCode,
  231. drugEntBaseInfoId: this.drugEntBaseInfoId,
  232. customerName: this.customerInput?.customerName,
  233. scanType: this.scanType,
  234. levelType: this.customerLevel,
  235. physicName: this.physicName == "全品种" ? "" : this.physicName,
  236. spkSpec: this.spkSpec == "全规格" ? "" : this.spkSpec,
  237. produceEntName: this.produceEntName == "全厂家" ? "" : this.produceEntName,
  238. };
  239. },
  240. tableBodyHeight() {
  241. // const rowHeight = 76; // rpx
  242. // if (this.totalCount >= 9) return `${rowHeight * 8.5}rpx`;
  243. // return "auto";
  244. const info = uni.getSystemInfoSync();
  245. const pixelRatio = 750 / info.windowWidth;
  246. const windowHeightRpx = this.windowHeight * pixelRatio;
  247. // Header card approx height: (statusBar + 60)px + content(approx 356rpx)
  248. const topPx = this.statusBarHeight + 60;
  249. const topRpx = topPx * pixelRatio + 356 + 24 + 24; // + margins
  250. const headerHeight = 80; // Table header
  251. return windowHeightRpx - topRpx - headerHeight - 20; // -20 safety
  252. },
  253. },
  254. methods: {
  255. closeDropdowns() {
  256. const r = this.$refs.scanRateTable;
  257. if (!r) return;
  258. if (typeof r.closeDropdown === "function") r.closeDropdown();
  259. if (typeof r.closeAllTooltip === "function") r.closeAllTooltip();
  260. },
  261. getEntNameList() {
  262. request(
  263. "/bills/getEntNameList",
  264. {
  265. path: "customerScanningRate/wigets/ScanRateTable.vue",
  266. },
  267. "get",
  268. ).then((res) => {
  269. if (res.code == 200) {
  270. const _data = res.data || [];
  271. this.entNames = [
  272. { entNameId: null, entName: "全厂家" },
  273. ..._data.map((item) => {
  274. return {
  275. entNameId: item,
  276. entName: String(item),
  277. selected: this.produceEntName == item,
  278. };
  279. }),
  280. ];
  281. }
  282. });
  283. },
  284. getProviceList() {
  285. request("/common/getProviceOfRegionList", {
  286. path: "customerScanningRate/wigets/ScanRateTable.vue",
  287. }).then((res) => {
  288. if (res.code == 200) {
  289. const _data = res.data;
  290. this.regions = [
  291. { regionCode: null, regionName: "全片区" },
  292. ..._data.map((item) => ({
  293. regionCode: item.regionCode,
  294. regionName: item.regionName,
  295. selected: this.regionCode == item.regionCode,
  296. })),
  297. ];
  298. }
  299. });
  300. },
  301. getDrugInfoName() {
  302. request("/bills/getDrugInfoName", {
  303. path: "customerScanningRate/wigets/ScanRateTable.vue",
  304. }).then((res) => {
  305. if (res.code == 200) {
  306. const _data = res.data;
  307. this.products = [
  308. {
  309. drugEntBaseInfoId: null,
  310. physicName: "全品种",
  311. pkgSpec: [],
  312. },
  313. ..._data.map((row) => ({
  314. drugEntBaseInfoId: row.drugEntBaseInfoId,
  315. physicName: row.physicName,
  316. selected: this.physicName == row.physicName,
  317. pkgSpec: (row.pkgSpec || []).map((sp) => ({
  318. pkgSpec: sp,
  319. pkgSpecName: String(sp),
  320. selected: this.spkSpec == sp,
  321. })),
  322. })),
  323. ];
  324. }
  325. });
  326. },
  327. safeDecode(value) {
  328. const raw = value == null ? "" : String(value);
  329. if (!raw) return "";
  330. try {
  331. return decodeURIComponent(raw);
  332. } catch (e) {
  333. return raw;
  334. }
  335. },
  336. buildSharePath() {
  337. return `/traceCodePackages/traceabilityReport/pages/customerScanningRate/detail/index?${this.buildShareQuery()}`;
  338. },
  339. buildShareQuery() {
  340. const type = this.rangeType;
  341. const regionCode = this.regionCode || "";
  342. const drugEntBaseInfoId = encodeURIComponent(this.drugEntBaseInfoId || "");
  343. const physicName = encodeURIComponent(this.physicName || "");
  344. const regionName = encodeURIComponent(this.regionName || "");
  345. const spkSpec = encodeURIComponent(this.spkSpec || "");
  346. const produceEntName = encodeURIComponent(this.produceEntName || "");
  347. const customerLevel = this.customerLevel || "";
  348. const scanType = this.scanType || "";
  349. return `type=${type}&regionCode=${regionCode}&drugEntBaseInfoId=${drugEntBaseInfoId}&physicName=${physicName}&regionName=${regionName}&spkSpec=${spkSpec}&produceEntName=${produceEntName}&scanType=${scanType}&customerLevel=${customerLevel}`;
  350. },
  351. getCustomerInfo() {
  352. if (!this.customerHasMore || this.customerLoading) return;
  353. this.customerLoading = true;
  354. request("/customer/info/getCustomerInfo", {
  355. customerName: this.customerInput?.customerName,
  356. name: this.physicName == "全品种" ? "" : this.physicName,
  357. drugEntBaseInfoId: this.drugEntBaseInfoId,
  358. spkSpec: this.spkSpec == "全规格" ? "" : this.spkSpec,
  359. produceEntName: this.produceEntName == "全厂家" ? "" : this.produceEntName,
  360. pageNum: this.customerPage,
  361. pageSize: this.customerPageSize,
  362. customerLevel: this.customerLevel,
  363. provinceCode: this.regionCode,
  364. path: "/traceabilityReport/pages/customerScanningRate/detail/index.vue",
  365. }).then((res) => {
  366. if (res.code == 200) {
  367. const _data = res.data || {};
  368. this.customerTotal = _data.total;
  369. if (this.customerTotal <= this.customers.length) {
  370. this.customerHasMore = false;
  371. return;
  372. } else {
  373. const list = Array.isArray(_data.list) ? _data.list : [];
  374. this.customers = [...this.customers, ...list];
  375. if (this.customerTotal <= this.customers.length) {
  376. this.customerHasMore = false;
  377. } else {
  378. this.customerPage++;
  379. this.customerHasMore = true;
  380. }
  381. // }
  382. }
  383. }
  384. setTimeout(() => {
  385. this.customerLoading = false;
  386. }, 200);
  387. });
  388. },
  389. openDropdown() {
  390. this.dropdownOpen = true;
  391. },
  392. toggleDropdown() {
  393. this.dropdownOpen = !this.dropdownOpen;
  394. },
  395. onInputChange(e) {
  396. const v = e?.detail?.value ?? this.customerInput;
  397. this.customerInput.customerName = v;
  398. },
  399. selectCustomer(c) {
  400. this.selectedCustomer = c;
  401. this.customerInput = c;
  402. this.dropdownOpen = false;
  403. },
  404. onTableFilterChange(filters) {
  405. this.regionCode = filters.regionCode || "";
  406. this.regionName = filters.regionName || "";
  407. this.drugEntBaseInfoId = filters.drugEntBaseInfoId || "";
  408. this.physicName = filters.physicName || "";
  409. this.spkSpec = filters.spkSpec || "";
  410. this.produceEntName = filters.produceEntName || "";
  411. this.customerPage = 1;
  412. this.customers = [];
  413. this.customerHasMore = true;
  414. this.getCustomerInfo();
  415. },
  416. closeDropdown() {
  417. this.dropdownOpen = false;
  418. },
  419. onBack() {
  420. try {
  421. uni.navigateBack();
  422. } catch (e) {}
  423. },
  424. onTableScrollToLower() {
  425. if (this.isLoading) return;
  426. if (this.list.length >= this.totalCount) return;
  427. this.isLoading = true;
  428. const remain = this.totalCount - this.list.length;
  429. const toAdd = Math.min(10, remain);
  430. setTimeout(() => {
  431. const more = this.genRows(toAdd);
  432. this.list = this.list.concat(more);
  433. this.isLoading = false;
  434. }, 800);
  435. },
  436. genRows(n) {
  437. const res = [];
  438. const regions = ["山东医药", "江苏医药", "浙江医药", "广东医药", "湖南医药", "湖北医药"];
  439. const base = this.list.length;
  440. for (let i = 0; i < n; i++) {
  441. const name = regions[(base + i) % regions.length];
  442. res.push({ region: name, outRate: "0%", inRate: "0%" });
  443. }
  444. return res;
  445. },
  446. },
  447. };
  448. </script>
  449. <style scoped>
  450. .nav {
  451. position: fixed;
  452. top: 0;
  453. left: 0;
  454. right: 0;
  455. height: 44px;
  456. background-color: #2c69ff;
  457. display: flex;
  458. align-items: center;
  459. justify-content: center;
  460. }
  461. .nav-title {
  462. font-size: 36rpx;
  463. color: #fff;
  464. font-weight: 700;
  465. }
  466. .nav-back {
  467. position: absolute;
  468. left: 40rpx;
  469. top: 12rpx;
  470. width: 20rpx;
  471. height: 20rpx;
  472. color: #fff;
  473. border-left: 3rpx solid #fff;
  474. border-bottom: 3rpx solid #fff;
  475. transform: rotate(45deg) translateY(56rpx);
  476. margin-left: 40rpx;
  477. }
  478. .page {
  479. height: 100vh;
  480. }
  481. .card {
  482. margin: 24rpx;
  483. background: #fff;
  484. border-radius: 16rpx;
  485. font-size: 32rpx;
  486. }
  487. /* .table-header {
  488. display: flex;
  489. background: #eaf2ff;
  490. font-weight: bold;
  491. color: #2c69ff;
  492. border-top-left-radius: 16rpx;
  493. border-top-right-radius: 16rpx;
  494. }
  495. .table-body {
  496. margin-top: 8rpx;
  497. }
  498. .row {
  499. display: flex;
  500. align-items: center;
  501. }
  502. .row:last-child {
  503. border-bottom: none;
  504. }
  505. .cell {
  506. flex: 1;
  507. display: flex;
  508. align-items: center;
  509. justify-content: center;
  510. height: 76rpx;
  511. } */
  512. .loading-row {
  513. justify-content: center;
  514. }
  515. .loading-wrapper {
  516. width: 100%;
  517. height: 76rpx;
  518. display: flex;
  519. align-items: center;
  520. justify-content: center;
  521. }
  522. .loading-icon {
  523. width: 40rpx;
  524. height: 40rpx;
  525. animation: spin 1s linear infinite;
  526. }
  527. @keyframes spin {
  528. from {
  529. transform: rotate(0deg);
  530. }
  531. to {
  532. transform: rotate(360deg);
  533. }
  534. }
  535. /* .total-row {
  536. font-weight: bold;
  537. color: #2c69ff;
  538. } */
  539. .header-card {
  540. background: #2c69ff;
  541. border-bottom-left-radius: 32rpx;
  542. border-bottom-right-radius: 32rpx;
  543. padding: 36rpx;
  544. color: #fff;
  545. }
  546. .meta {
  547. display: flex;
  548. align-items: center;
  549. margin: 14rpx 0;
  550. }
  551. .dot {
  552. width: 8rpx;
  553. height: 8rpx;
  554. border-radius: 50%;
  555. background: #fff;
  556. margin-right: 20rpx;
  557. }
  558. .meta-text {
  559. font-size: 32rpx;
  560. }
  561. .selector-wrap {
  562. position: relative;
  563. margin-top: 16rpx;
  564. }
  565. .selector-input {
  566. width: 100%;
  567. height: 64rpx;
  568. border-radius: 32rpx;
  569. background: #fff;
  570. color: #333;
  571. padding: 0 72rpx 0 30rpx;
  572. box-sizing: border-box;
  573. font-size: 28rpx;
  574. }
  575. .selector-arrow {
  576. position: absolute;
  577. right: 24rpx;
  578. bottom: 25rpx;
  579. width: 16rpx;
  580. height: 16rpx;
  581. border: 5rpx solid #2c69ff;
  582. border-top: none;
  583. border-left: none;
  584. transform-origin: 50% 50%;
  585. transform: rotate(45deg);
  586. transition: transform 0.2s;
  587. }
  588. .selector-arrow.open {
  589. bottom: 18rpx;
  590. transform: rotate(225deg);
  591. }
  592. .dropdown {
  593. position: absolute;
  594. left: 0;
  595. right: 0;
  596. top: 70rpx;
  597. background: #fff;
  598. border: 1rpx solid #eef0f4;
  599. border-radius: 12rpx;
  600. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
  601. z-index: 10;
  602. max-height: none;
  603. }
  604. .dropdown-item {
  605. padding: 16rpx 24rpx;
  606. font-size: 28rpx;
  607. color: #333;
  608. }
  609. .dropdown-item + .dropdown-item {
  610. border-top: 1rpx solid #f0f2f5;
  611. }
  612. .customer-loading-wrap {
  613. width: 100%;
  614. height: 70rpx;
  615. display: flex;
  616. align-items: center;
  617. justify-content: center;
  618. }
  619. </style>