ProductStore.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. <template>
  2. <div class="product-store-container flex flex-col">
  3. <div class="product-store__header">
  4. <div class="search-box" v-if="type === '1'">
  5. <van-field v-model="searchVal" center placeholder="搜索" left-icon="search" @click="handleClickSearchBox">
  6. <template #button>
  7. <van-button size="small" @click="handleLaunchSearch" type="primary">搜索</van-button>
  8. </template>
  9. </van-field>
  10. </div>
  11. <div class="sub-title flex flex-row flex-row-aic flex-row-jcsp">
  12. <span v-if="type === '2'">请选择商品分类</span>
  13. <span v-else>商品库</span>
  14. <van-button v-show="searchData.length" size="small" @click="handleResetSearch" type="primary">重置搜索</van-button>
  15. </div>
  16. </div>
  17. <!-- 展示内容 -->
  18. <div class="product-store__main" v-if="searchData.length">
  19. <div class="search-row" v-for="(good, idx) in searchData" :key="idx" @click="handleSelectedGoods(good)">
  20. <div class="search-row__category">
  21. {{ good.goods_category_first_en }}/{{ good.goods_category_en }}
  22. </div>
  23. <div class="search-row__title ellipsis">{{ good.goods_name }}</div>
  24. </div>
  25. </div>
  26. <!-- 分类内容 -->
  27. <div class="product-store__main" v-else>
  28. <van-radio-group v-model="radioCategory">
  29. <div class="row" v-for="(item, idx) in categoryData" :key="idx">
  30. <div class="row__header row__header--b-line flex flex-row flex-row-aic">
  31. <div class="row__header__content">
  32. {{ item.name }}
  33. </div>
  34. <div class="row__header__more flex flex-row flex-row-aic" @click="handleClickRow(item, idx)">
  35. <van-icon name="cluster-o" :size="26" color="#3290C4" />
  36. <span class="txt">下级</span>
  37. </div>
  38. </div>
  39. <div class="row__main" v-if="item.expand">
  40. <div class="row__second" v-for="(second, idx2) in item.childlist" :key="idx2">
  41. <div class="row__second__header row__second__header--b-line flex flex-row flex-row-aic"
  42. @click="type === '1' ? handleClickSecondRow(second, idx2) : null">
  43. <!-- NOTE: Radio. 单选组件 -->
  44. <div class="radio-box" v-if="type === '2'">
  45. <van-radio :name="second.id">{{ second.name }}</van-radio>
  46. </div>
  47. <div v-else class="row__second__header__content">
  48. {{ second.name }}
  49. </div>
  50. <!-- NOTE: 只存在于选择商品 -->
  51. <div class="row__second__header__more" v-if="type === '1'">
  52. <van-icon v-if="second.expand" name="arrow-up" :size="24" color="rgba(162, 163, 164, 1)" />
  53. <van-icon v-else name="arrow-down" :size="24" color="rgba(162, 163, 164, 1)" />
  54. </div>
  55. </div>
  56. <div class="row__second__main" v-if="second.expand && second.childlist">
  57. <div class="row__third-item flex flex-row flex-row-aic" v-for="(good, idx3) in second.childlist"
  58. :key="idx3" @click="handleSelectedGoods(good, idx3)">
  59. <span id="c1">{{ good.goods_name }}</span>
  60. </div>
  61. </div>
  62. <div class="row__second__main" v-else>
  63. <div class="row__third-item flex flex-row flex-row-aic">
  64. <span id="c1">当前分类下暂无商品</span>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. </van-radio-group>
  71. </div>
  72. <!-- 提交 -->
  73. <div class="btn-container" v-if="type === '2'">
  74. <div class="btn-span" @click="handleConfirmCategory">确定</div>
  75. </div>
  76. <van-popup class="popup flex flex-col" v-model="popupVisibility" position="bottom" :style="{ height: '60%' }"
  77. closeable close-icon-position="top-right">
  78. <div class="popup__header">
  79. <span class="product__title" v-if="choosedStock">{{ choosedStock.goods_name }}</span>
  80. <span class="product__inventory">{{ leftStockComp }}</span>
  81. </div>
  82. <div class="popup__main">
  83. <template v-for="(item, idx) in productData">
  84. <template v-if="item.type === 'total'">
  85. <c-input :title="item.label" :placeholder="`请输入${item.label}`" :maxlength="5" :showWordLimit="false"
  86. input-type="digit" v-model="item.val" :key="idx" />
  87. </template>
  88. <template v-else-if="item.type === 'price'">
  89. <c-input :title="item.label" :placeholder="`请输入${item.label}`" :maxlength="5" :showWordLimit="false"
  90. input-type="number" v-model="item.val" :key="idx" />
  91. </template>
  92. <template v-else>
  93. <div :key="idx" class="product__row">
  94. <div class="product__row__header">
  95. {{ item.label }}
  96. </div>
  97. <div class="product__row__main">
  98. <span v-for="(type, idx) in item.list" :key="idx" :data-id="type.id"
  99. :class="{ 'selected': item.val == type.id }" @click="handleClickItem(item, type)">{{ type.name }}</span>
  100. </div>
  101. </div>
  102. </template>
  103. </template>
  104. </div>
  105. <div class="popup__footer">
  106. <div class="btn-container">
  107. <div class="btn-span" @click="handleConfirmInput">确认</div>
  108. </div>
  109. </div>
  110. </van-popup>
  111. </div>
  112. </template>
  113. <style lang="less" scoped>
  114. @import url('@/styles/variables.less');
  115. .product-store {
  116. &-container {
  117. height: 100vh;
  118. justify-content: space-between;
  119. }
  120. &__header {
  121. padding: 10px 12px 0;
  122. background-color: @white;
  123. .search-box {
  124. .van-cell.van-field {
  125. background: rgba(118, 118, 128, 0.12);
  126. border-radius: 8px;
  127. }
  128. }
  129. .sub-title {
  130. font-size: @font-size-third;
  131. font-weight: 400;
  132. color: #727273;
  133. line-height: 18px;
  134. padding: 10px 0;
  135. }
  136. .van-button--primary {
  137. background-color: @main-color;
  138. border: 1px solid @main-color;
  139. }
  140. }
  141. &__main {
  142. padding: 10px 0;
  143. height: 0;
  144. flex: 1;
  145. .row {
  146. margin-bottom: 10px;
  147. background-color: @white;
  148. &__header {
  149. padding: 10px 12px;
  150. justify-content: space-between;
  151. &--b-line {
  152. border-bottom: 1px solid #eee;
  153. }
  154. &__content {
  155. font-size: @font-size-secondery;
  156. }
  157. &__more {
  158. font-size: 16px;
  159. border-left: 1px solid rgba(151, 151, 151, 0.4);
  160. padding: 3px 10px 3px 20px;
  161. span.txt {
  162. padding-left: 6px;
  163. color: #3290c4;
  164. }
  165. }
  166. }
  167. &__main {
  168. padding-left: 36px;
  169. }
  170. &__second {
  171. &__header {
  172. padding: 10px 12px;
  173. justify-content: space-between;
  174. &--b-line {
  175. border-bottom: 1px solid #eee;
  176. }
  177. &__content {
  178. font-size: @font-size-secondery;
  179. }
  180. &__more {}
  181. /deep/.van-radio__label {
  182. font-size: 14px;
  183. }
  184. // .van-radio__icon--checked .van-icon
  185. /deep/.van-radio__icon--checked .van-icon {
  186. background-color: @main-color;
  187. border-color: @main-color;
  188. }
  189. }
  190. &__main {
  191. padding-left: 30px;
  192. }
  193. }
  194. &__third-item {
  195. padding: 10px 12px;
  196. font-size: @font-size-third;
  197. border-bottom: 1px solid #eee;
  198. &:last-child {
  199. border-bottom-color: transparent;
  200. }
  201. }
  202. }
  203. .search-row {
  204. padding: 10px 10px;
  205. background-color: #fff;
  206. &__category {
  207. font-size: 12px;
  208. color: #8d8c8c;
  209. margin-bottom: 8px;
  210. }
  211. &__title {
  212. font-size: 16px;
  213. }
  214. }
  215. }
  216. }
  217. .popup {
  218. justify-content: space-between;
  219. padding: 8px 0 0;
  220. box-sizing: border-box;
  221. &__header {
  222. margin: 0 12px;
  223. padding-right: 30px;
  224. padding-bottom: 10px;
  225. padding-top: 8px;
  226. border-bottom: 1px solid rgba(151, 151, 151, 0.3);
  227. font-size: @font-size-common;
  228. }
  229. &__main {
  230. margin: 18px 0;
  231. height: 0;
  232. flex: 1;
  233. overflow: auto;
  234. .layout-container {
  235. padding-top: 12px;
  236. }
  237. }
  238. &__footer {
  239. position: relative;
  240. z-index: 9;
  241. border-top: 12px solid rgba(248, 248, 248, 1);
  242. box-shadow: 0 -2px 16px 1px rgba(0, 0, 0, 0.2);
  243. .btn-container {
  244. margin-top: initial;
  245. }
  246. }
  247. }
  248. .product {
  249. &__row {
  250. padding: 6px 12px 16px;
  251. &:nth-last-of-type(2) {
  252. border-bottom: 1px solid rgba(151, 151, 151, 0.3);
  253. }
  254. }
  255. &__title {
  256. font-size: 16px;
  257. font-weight: 500;
  258. color: #191A1E;
  259. }
  260. &__inventory {
  261. padding-left: 10px;
  262. font-size: 12px;
  263. font-weight: 400;
  264. color: #727273;
  265. vertical-align: bottom;
  266. }
  267. &__row {
  268. &__header {
  269. font-size: @font-size-common;
  270. padding: 8px 0 10px;
  271. color: #191A1E;
  272. }
  273. &__main {
  274. display: grid;
  275. grid-template-columns: repeat(2, 49%);
  276. column-gap: 2%;
  277. row-gap: 10px;
  278. span {
  279. display: inline-block;
  280. text-align: center;
  281. background: #EFF7FB;
  282. border-radius: 8px;
  283. font-size: @font-size-secondery;
  284. font-weight: 400;
  285. color: #727273;
  286. padding: 8px 0;
  287. transition-property: background-color, color;
  288. transition-duration: 0.2s;
  289. &.selected {
  290. background: #0a83d3;
  291. color: @white;
  292. }
  293. }
  294. }
  295. }
  296. }
  297. </style>
  298. <script>
  299. /**
  300. * 商品库页面
  301. * @description 当前页面处理为多种展示格式。
  302. * - 商品单选
  303. * - 分类选择
  304. */
  305. import vueBus from '@/utils/vueBus';
  306. import CInput from './components/CInput.vue';
  307. import * as goodsApi from '@/api/goods'
  308. export default {
  309. name: "ProductStore",
  310. components: {
  311. CInput
  312. },
  313. computed: {
  314. leftStockComp() {
  315. if (!this.choosedStock) return ''
  316. const [{ val, list }] = this.productData
  317. let currentData = list.filter(item => item.id === val)
  318. if (currentData.length) {
  319. let len = currentData[0].stock
  320. return len > 0 ? `库存剩余:${len}件` : `暂无库存`
  321. } else return ''
  322. }
  323. },
  324. data: () => ({
  325. type: "1", // 页面状态。 默认1:商品选择。 可选项2:选择分类
  326. module: '', // 进入module不同时有些许差别
  327. radioCategory: '', // 二级分类Id
  328. searchVal: '', // 搜索商品?
  329. categoryData: [], // 分类数据
  330. // NOTE: Popup data and context data
  331. popupVisibility: false,
  332. productData: [], // 弹出选择商品的数量等。
  333. choosedStock: null,
  334. searchData: [], // 搜索数据存储列表
  335. }),
  336. created() {
  337. this.__init__()
  338. },
  339. methods: {
  340. async __init__() {
  341. try {
  342. let query = this.$route.query
  343. if (query) {
  344. // type = 1 // 页面类型。1 => 选择商品。 2 => 选择分类
  345. if (query.type) this.type = query.type
  346. // 申请的`module`
  347. if (query.module) {
  348. this.module = query.module
  349. }
  350. }
  351. await this.__queryCategoay__() // initalization query categray
  352. } catch (error) {
  353. console.log('ProductStore __init__ error', error);
  354. }
  355. },
  356. handleClickSearchBox() { },
  357. // 点击一级
  358. handleClickRow(row) {
  359. row.expand = !row.expand
  360. this.$forceUpdate()
  361. },
  362. // 点击二级
  363. async handleClickSecondRow(row) {
  364. const toastInstance = this.$toast({
  365. type: 'loading',
  366. message: '加载数据中',
  367. duration: 0
  368. })
  369. try {
  370. if (!row.expand && !row.childlist) { // NOTE: 只有打开时&无子级别查询
  371. const list = await this.__queryStoreData__(row.id)
  372. row.childlist = [...list]
  373. }
  374. row.expand = !row.expand
  375. this.$forceUpdate()
  376. } catch (error) {
  377. this.$toast(error.message)
  378. } finally {
  379. toastInstance.clear()
  380. }
  381. },
  382. // 选择类型
  383. handleClickItem(row, item) {
  384. row.val = item.id
  385. },
  386. // NOTE: popup confirm 确认数据正常
  387. handleConfirmInput() {
  388. try {
  389. let _list = this.productData
  390. let isAllInput = _list.every(item => item.val)
  391. if (!isAllInput) return this.$toast('检查填写情况')
  392. const [choosedGoodsId, customCount, GoodsPrice] = _list.map(item => item.val)
  393. // 选中的类别对象
  394. let choosedGoodsS = _list[0].list.filter(item => item.id === choosedGoodsId)[0]
  395. let ChoosedGoodsStock = 0
  396. if (choosedGoodsS) {
  397. ChoosedGoodsStock = choosedGoodsS.stock
  398. }
  399. // 判断输入的数量是否大于库存
  400. if (customCount <= 0) {
  401. return this.$toast('物品数量最少1件')
  402. }
  403. if (this.module != 3 && customCount > ChoosedGoodsStock) {
  404. return this.$toast('当前商品规格数量不足')
  405. }
  406. if (GoodsPrice <= 0) {
  407. return this.$toast('当前商品价格不对')
  408. }
  409. // if (this.type === '1') {}
  410. vueBus.$emit('updateProductList', {
  411. item: this.choosedStock,
  412. goodsStock: choosedGoodsS,
  413. customCount,
  414. GoodsPrice
  415. })
  416. this.$router.go(-1);
  417. } catch (error) {
  418. console.log(error);
  419. }
  420. },
  421. // NOTE: 选中商品进行业务
  422. handleSelectedGoods(item) {
  423. const { goods_stock } = item
  424. this.choosedStock = item
  425. let temporaryData = [
  426. {
  427. label: '规格',
  428. val: '',
  429. list: goods_stock
  430. },
  431. {
  432. label: '物品数量',
  433. type: 'total',
  434. val: ''
  435. }
  436. ]
  437. // 申购明细时需要物品单价
  438. if (['1'].includes(this.module)) {
  439. temporaryData.push({
  440. label: '物品单价',
  441. type: 'price',
  442. val: ''
  443. })
  444. }
  445. this.productData = temporaryData
  446. this.popupVisibility = true
  447. },
  448. // NOTE: 查询分类数据
  449. __queryCategoay__() {
  450. return new Promise((resolve, reject) => {
  451. goodsApi.category().then(result => {
  452. if (result.code === 1) {
  453. this.categoryData = result.data
  454. resolve()
  455. } else {
  456. reject()
  457. }
  458. }).catch(error => {
  459. console.log('%c error >>>', 'background: blue; color: #fff', error);
  460. reject()
  461. })
  462. })
  463. },
  464. // NOTE: 查询商品数据
  465. // @returns [] | throw error
  466. __queryStoreData__(category_id) {
  467. return new Promise((resolve, reject) => {
  468. goodsApi.list({
  469. category_id
  470. }).then(result => {
  471. if (result.code === 1) {
  472. resolve(result.data || [])
  473. }
  474. }).catch(error => {
  475. reject(error)
  476. })
  477. })
  478. },
  479. // NOTE: 确定选择的分类
  480. handleConfirmCategory() {
  481. let selId = this.radioCategory
  482. let arrs = this.categoryData
  483. for (let i = 0; i < arrs.length; i++) {
  484. const row = arrs[i];
  485. const childlist = row.childlist
  486. for (let j = 0; j < childlist.length; j++) {
  487. const item = childlist[j];
  488. if (selId === item.id) {
  489. let first = { ...row }
  490. delete first.childlist
  491. delete first.expand
  492. vueBus.$emit('listenCategoryEvent', {
  493. first,
  494. second: { ...item }
  495. })
  496. break
  497. } else continue
  498. }
  499. }
  500. this.$router.go(-1)
  501. },
  502. // NOTE: 根据id展示分类名称
  503. __category_en__(good) {
  504. const { goods_category_first, goods_category_id } = good
  505. const temporary = {
  506. goods_category_first_en: '',
  507. goods_category_en: ''
  508. }
  509. let arrs = this.categoryData
  510. for (let i = 0; i < arrs.length; i++) {
  511. const firs = arrs[i];
  512. if (firs.id === goods_category_first) {
  513. for (let j = 0; j < firs.childlist.length; j++) {
  514. const itm = firs.childlist[j];
  515. if (itm.id === goods_category_id) {
  516. temporary.goods_category_first_en = firs.name
  517. temporary.goods_category_en = itm.name
  518. break
  519. } else continue
  520. }
  521. } else continue
  522. }
  523. return temporary
  524. },
  525. // NOTE: Search Event
  526. async handleLaunchSearch() {
  527. let keyword = this.searchVal
  528. if (!keyword) return
  529. const toastInstance = this.$toast({
  530. type: 'loading',
  531. message: '搜索中...',
  532. duration: 0
  533. })
  534. try {
  535. const result = await goodsApi.list({
  536. search: keyword
  537. })
  538. if (result.code === 1) {
  539. if (result.data.length) {
  540. this.searchData = result.data.map(good => ({
  541. ...good,
  542. ...this.__category_en__(good)
  543. }))
  544. toastInstance.clear()
  545. } else {
  546. toastInstance.clear()
  547. this.$toast(`暂无与${keyword}相关商品`)
  548. }
  549. }
  550. } catch (error) {
  551. toastInstance.clear()
  552. console.log('%c handleLaunchSearchError >>>', 'background: blue; color: #fff', error);
  553. }
  554. },
  555. // Reset search
  556. handleResetSearch() {
  557. this.searchData = []
  558. this.searchVal = ''
  559. },
  560. },
  561. watch: {
  562. radio(val) {
  563. console.log('%c radio value watch? >>>', 'background: blue; color: #fff', val);
  564. }
  565. }
  566. }
  567. </script>