apply.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. <template>
  2. <view class="page">
  3. <NavBar title="新品申请" :showBack="true" />
  4. <view class="content">
  5. <view class="search-bar">
  6. <view class="search-input-wrap">
  7. <view class="search-icon"></view>
  8. <input
  9. class="search-input"
  10. v-model="searchKeyword"
  11. placeholder="搜索商品名称或条形码"
  12. confirm-type="search"
  13. @confirm="onSearch"
  14. />
  15. <view v-if="searchKeyword" class="search-clear" @click="clearSearch">
  16. <view class="clear-icon"></view>
  17. </view>
  18. </view>
  19. <view class="filter-btn" @click="showFilter = !showFilter">
  20. <text :class="['filter-text', { active: form.status !== '' || form.applicantId }]">筛选</text>
  21. </view>
  22. <view class="reset-btn" @click="resetForm">
  23. <text class="reset-text">重置</text>
  24. </view>
  25. </view>
  26. <view class="filter-panel" v-if="showFilter">
  27. <view class="filter-section">
  28. <text class="filter-section-label">申请状态</text>
  29. <view class="filter-tags">
  30. <view
  31. class="filter-tag"
  32. :class="{ active: form.status === '' }"
  33. @click="selectStatus('')"
  34. >
  35. <text>全部</text>
  36. </view>
  37. <view
  38. class="filter-tag"
  39. :class="{ active: form.status === 0 }"
  40. @click="selectStatus(0)"
  41. >
  42. <text>待提交</text>
  43. </view>
  44. <view
  45. class="filter-tag"
  46. :class="{ active: form.status === 1 }"
  47. @click="selectStatus(1)"
  48. >
  49. <text>待审核</text>
  50. </view>
  51. <view
  52. class="filter-tag"
  53. :class="{ active: form.status === 2 }"
  54. @click="selectStatus(2)"
  55. >
  56. <text>已通过</text>
  57. </view>
  58. <view
  59. class="filter-tag"
  60. :class="{ active: form.status === 3 }"
  61. @click="selectStatus(3)"
  62. >
  63. <text>已拒绝</text>
  64. </view>
  65. </view>
  66. </view>
  67. <view class="filter-section">
  68. <text class="filter-section-label">申请人</text>
  69. <view class="filter-input-wrap">
  70. <input
  71. class="filter-input"
  72. v-model="applicantNameInput"
  73. placeholder="请输入申请人"
  74. confirm-type="search"
  75. @confirm="onSearch"
  76. />
  77. </view>
  78. </view>
  79. </view>
  80. <scroll-view
  81. class="list-scroll"
  82. scroll-y
  83. @scrolltolower="loadMore"
  84. refresher-enabled
  85. :refresher-triggered="refreshing"
  86. @refresherrefresh="onRefresh"
  87. >
  88. <view v-if="dataList.length === 0 && !loading" class="empty-state">
  89. <view class="empty-icon"></view>
  90. <text class="empty-title">暂无申请记录</text>
  91. <text class="empty-desc">提交新品上架申请</text>
  92. <view class="empty-btn" @click="goApplyForm()">
  93. <text class="empty-btn-text">立即申请</text>
  94. </view>
  95. </view>
  96. <view
  97. class="apply-card"
  98. v-for="item in dataList"
  99. :key="item.id"
  100. >
  101. <view class="apply-header" @click="goDetail(item)">
  102. <view class="apply-id-wrap">
  103. <text class="apply-id">#{{ item.id }}</text>
  104. <text class="apply-name">{{ item.productName }}</text>
  105. </view>
  106. <view :class="['status-tag', getStatusClass(item.status)]">
  107. <text class="status-text">{{ getStatusText(item.status) }}</text>
  108. </view>
  109. </view>
  110. <view class="apply-info" @click="goDetail(item)">
  111. <view class="info-row">
  112. <text class="info-label">条形码</text>
  113. <text class="info-value">{{ item.barcode || '-' }}</text>
  114. </view>
  115. <view class="info-row">
  116. <text class="info-label">分类</text>
  117. <text class="info-value">{{ item.category || '-' }}</text>
  118. </view>
  119. <view class="info-row">
  120. <text class="info-label">品牌</text>
  121. <text class="info-value">{{ item.brand || '-' }}</text>
  122. </view>
  123. <view class="info-row">
  124. <text class="info-label">售价</text>
  125. <text class="info-value price">¥{{ formatMoney(item.price || 0) }}</text>
  126. </view>
  127. <view class="info-row">
  128. <text class="info-label">申请人</text>
  129. <text class="info-value">{{ item.applicantName || '-' }}</text>
  130. </view>
  131. <view class="info-row">
  132. <text class="info-label">申请时间</text>
  133. <text class="info-value">{{ item.applyTime ? formatDateTime(item.applyTime) : '-' }}</text>
  134. </view>
  135. </view>
  136. <view class="apply-footer">
  137. <view class="footer-left">
  138. <view class="action-btn view" @click="goDetail(item)">
  139. <text>查看</text>
  140. </view>
  141. <view
  142. v-if="item.status === 0"
  143. class="action-btn edit"
  144. @click="goApplyForm(item)"
  145. >
  146. <text>编辑</text>
  147. </view>
  148. <view
  149. v-if="item.status === 0"
  150. class="action-btn delete"
  151. @click.stop="handleDelete(item)"
  152. >
  153. <text>删除</text>
  154. </view>
  155. </view>
  156. </view>
  157. </view>
  158. <view class="load-more" v-if="dataList.length > 0">
  159. <view class="load-more-row" v-if="loading">
  160. <view class="load-spinner"></view>
  161. <text class="load-more-text">加载中...</text>
  162. </view>
  163. <text class="load-more-text" v-else-if="noMore">没有更多了</text>
  164. </view>
  165. </scroll-view>
  166. </view>
  167. <view class="fab-btn" @click="goApplyForm()">
  168. <text class="fab-icon">+</text>
  169. </view>
  170. </view>
  171. </template>
  172. <script setup lang="ts">
  173. import { ref, reactive, onMounted } from 'vue';
  174. import NavBar from '@/components/NavBar.vue';
  175. import {
  176. getNewProductApplyList,
  177. deleteNewProductApply,
  178. type NewProductApplyItem
  179. } from '@/api/newProductApply';
  180. import { formatMoney, formatDateTime, showConfirm, showToast } from '@/utils/common';
  181. const form = reactive({
  182. productName: '',
  183. barcode: '',
  184. status: '' as number | '',
  185. applicantId: '' as number | ''
  186. });
  187. const searchKeyword = ref('');
  188. const applicantNameInput = ref('');
  189. const showFilter = ref(false);
  190. const dataList = ref<NewProductApplyItem[]>([]);
  191. const loading = ref(false);
  192. const refreshing = ref(false);
  193. const noMore = ref(false);
  194. const pagination = reactive({
  195. page: 1,
  196. pageSize: 10,
  197. total: 0
  198. });
  199. const getStatusText = (status?: number) => {
  200. const map: Record<number, string> = {
  201. 0: '待提交',
  202. 1: '待审核',
  203. 2: '已通过',
  204. 3: '已拒绝'
  205. };
  206. return map[status ?? -1] || '未知';
  207. };
  208. const getStatusClass = (status?: number) => {
  209. const map: Record<number, string> = {
  210. 0: 'draft',
  211. 1: 'pending',
  212. 2: 'approved',
  213. 3: 'rejected'
  214. };
  215. return map[status ?? -1] || 'draft';
  216. };
  217. const selectStatus = (status: number | '') => {
  218. form.status = status;
  219. pagination.page = 1;
  220. noMore.value = false;
  221. loadData();
  222. };
  223. const onSearch = () => {
  224. const keyword = searchKeyword.value.trim();
  225. form.productName = keyword;
  226. form.barcode = keyword;
  227. if (applicantNameInput.value.trim()) {
  228. form.applicantId = Number(applicantNameInput.value.trim()) || '';
  229. } else {
  230. form.applicantId = '';
  231. }
  232. pagination.page = 1;
  233. noMore.value = false;
  234. loadData();
  235. };
  236. const clearSearch = () => {
  237. searchKeyword.value = '';
  238. form.productName = '';
  239. form.barcode = '';
  240. pagination.page = 1;
  241. noMore.value = false;
  242. loadData();
  243. };
  244. const resetForm = () => {
  245. searchKeyword.value = '';
  246. applicantNameInput.value = '';
  247. form.productName = '';
  248. form.barcode = '';
  249. form.status = '';
  250. form.applicantId = '';
  251. showFilter.value = false;
  252. pagination.page = 1;
  253. noMore.value = false;
  254. loadData();
  255. };
  256. const loadData = async () => {
  257. loading.value = true;
  258. try {
  259. const params: any = {
  260. page: pagination.page,
  261. pageSize: pagination.pageSize
  262. };
  263. if (form.productName) params.productName = form.productName;
  264. if (form.barcode) params.barcode = form.barcode;
  265. if (form.status !== '') params.status = form.status;
  266. if (form.applicantId) params.applicantId = form.applicantId;
  267. const res = await getNewProductApplyList(params);
  268. if (res) {
  269. const list = res.list || [];
  270. if (pagination.page === 1) {
  271. dataList.value = list;
  272. } else {
  273. dataList.value = [...dataList.value, ...list];
  274. }
  275. pagination.total = res.total || 0;
  276. noMore.value = dataList.value.length >= pagination.total;
  277. }
  278. } catch (error) {
  279. console.error('加载新品申请列表失败', error);
  280. } finally {
  281. loading.value = false;
  282. refreshing.value = false;
  283. }
  284. };
  285. const onRefresh = async () => {
  286. refreshing.value = true;
  287. pagination.page = 1;
  288. noMore.value = false;
  289. await loadData();
  290. };
  291. const loadMore = () => {
  292. if (loading.value || noMore.value) return;
  293. pagination.page++;
  294. loadData();
  295. };
  296. const handleDelete = async (item: NewProductApplyItem) => {
  297. const confirmed = await showConfirm('是否确认删除?');
  298. if (!confirmed) return;
  299. try {
  300. await deleteNewProductApply(item.id!);
  301. showToast('删除成功', 'success');
  302. pagination.page = 1;
  303. noMore.value = false;
  304. await loadData();
  305. } catch (error) {
  306. console.error('删除失败', error);
  307. }
  308. };
  309. const goApplyForm = (item?: NewProductApplyItem) => {
  310. const url = item?.id
  311. ? `/pages/products/apply-form?id=${item.id}`
  312. : '/pages/products/apply-form';
  313. uni.navigateTo({ url });
  314. };
  315. const goDetail = (item: NewProductApplyItem) => {
  316. uni.navigateTo({ url: `/pages/products/apply-detail?id=${item.id}` });
  317. };
  318. onMounted(() => {
  319. loadData();
  320. });
  321. </script>
  322. <style lang="scss" scoped>
  323. .page {
  324. min-height: 100vh;
  325. background: $bg-color-page;
  326. display: flex;
  327. flex-direction: column;
  328. }
  329. .content {
  330. flex: 1;
  331. display: flex;
  332. flex-direction: column;
  333. overflow: hidden;
  334. }
  335. .search-bar {
  336. display: flex;
  337. align-items: center;
  338. gap: 12rpx;
  339. padding: 20rpx 24rpx;
  340. }
  341. .search-input-wrap {
  342. flex: 1;
  343. display: flex;
  344. align-items: center;
  345. background: $bg-color-card;
  346. border-radius: $radius-full;
  347. padding: 0 24rpx;
  348. height: 72rpx;
  349. }
  350. .search-icon {
  351. width: 32rpx;
  352. height: 32rpx;
  353. margin-right: 12rpx;
  354. border: 3rpx solid $text-color-muted;
  355. border-radius: 50%;
  356. position: relative;
  357. &::after {
  358. content: '';
  359. position: absolute;
  360. bottom: -4rpx;
  361. right: -4rpx;
  362. width: 8rpx;
  363. height: 3rpx;
  364. background: $text-color-muted;
  365. border-radius: 3rpx;
  366. transform: rotate(45deg);
  367. transform-origin: left center;
  368. }
  369. }
  370. .search-input {
  371. flex: 1;
  372. height: 72rpx;
  373. font-size: $font-size-sm;
  374. color: $text-color-primary;
  375. }
  376. .search-clear {
  377. width: 40rpx;
  378. height: 40rpx;
  379. display: flex;
  380. align-items: center;
  381. justify-content: center;
  382. border-radius: 50%;
  383. background: $bg-color-secondary;
  384. &:active { opacity: 0.7; }
  385. }
  386. .clear-icon {
  387. width: 20rpx;
  388. height: 20rpx;
  389. position: relative;
  390. &::before,
  391. &::after {
  392. content: '';
  393. position: absolute;
  394. top: 50%;
  395. left: 50%;
  396. width: 16rpx;
  397. height: 2rpx;
  398. background: $text-color-muted;
  399. border-radius: 1rpx;
  400. }
  401. &::before { transform: translate(-50%, -50%) rotate(45deg); }
  402. &::after { transform: translate(-50%, -50%) rotate(-45deg); }
  403. }
  404. .filter-btn {
  405. padding: 0 20rpx;
  406. height: 72rpx;
  407. display: flex;
  408. align-items: center;
  409. justify-content: center;
  410. background: $bg-color-card;
  411. border-radius: $radius-full;
  412. &:active { opacity: 0.8; }
  413. }
  414. .filter-text {
  415. font-size: $font-size-sm;
  416. color: $text-color-secondary;
  417. &.active {
  418. color: $primary-color;
  419. font-weight: 600;
  420. }
  421. }
  422. .reset-btn {
  423. padding: 0 20rpx;
  424. height: 72rpx;
  425. display: flex;
  426. align-items: center;
  427. justify-content: center;
  428. background: $bg-color-card;
  429. border-radius: $radius-full;
  430. &:active { opacity: 0.8; }
  431. }
  432. .reset-text {
  433. font-size: $font-size-sm;
  434. color: $text-color-muted;
  435. }
  436. .filter-panel {
  437. padding: 8rpx 24rpx 20rpx;
  438. background: $bg-color-card;
  439. margin: 0 24rpx;
  440. border-radius: $radius-lg;
  441. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  442. }
  443. .filter-section {
  444. padding: 14rpx 0;
  445. & + .filter-section {
  446. padding-top: 18rpx;
  447. }
  448. }
  449. .filter-section-label {
  450. font-size: $font-size-sm;
  451. color: $text-color-secondary;
  452. font-weight: 500;
  453. margin-bottom: 12rpx;
  454. display: block;
  455. }
  456. .filter-tags {
  457. display: flex;
  458. gap: 16rpx;
  459. flex-wrap: wrap;
  460. }
  461. .filter-tag {
  462. padding: 10rpx 28rpx;
  463. background: #FAFAFA;
  464. border-radius: $radius-full;
  465. text {
  466. font-size: $font-size-sm;
  467. color: $text-color-secondary;
  468. }
  469. &.active {
  470. background: $primary-color-bg;
  471. text {
  472. color: $primary-color-dark;
  473. font-weight: 600;
  474. }
  475. }
  476. }
  477. .filter-input-wrap {
  478. background: #FAFAFA;
  479. border-radius: $radius-base;
  480. padding: 0 20rpx;
  481. height: 72rpx;
  482. display: flex;
  483. align-items: center;
  484. }
  485. .filter-input {
  486. flex: 1;
  487. height: 72rpx;
  488. font-size: $font-size-sm;
  489. color: $text-color-primary;
  490. }
  491. .list-scroll {
  492. flex: 1;
  493. padding: 0 24rpx;
  494. height: 0;
  495. }
  496. .apply-card {
  497. background: $bg-color-card;
  498. border-radius: $radius-lg;
  499. padding: 24rpx;
  500. margin-bottom: 16rpx;
  501. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  502. }
  503. .apply-header {
  504. display: flex;
  505. align-items: flex-start;
  506. justify-content: space-between;
  507. margin-bottom: 16rpx;
  508. }
  509. .apply-id-wrap {
  510. flex: 1;
  511. display: flex;
  512. align-items: center;
  513. margin-right: 16rpx;
  514. }
  515. .apply-id {
  516. font-size: $font-size-xs;
  517. color: $text-color-muted;
  518. margin-right: 12rpx;
  519. flex-shrink: 0;
  520. }
  521. .apply-name {
  522. font-size: $font-size-lg;
  523. font-weight: 600;
  524. color: $text-color-primary;
  525. flex: 1;
  526. overflow: hidden;
  527. text-overflow: ellipsis;
  528. white-space: nowrap;
  529. }
  530. .status-tag {
  531. padding: 6rpx 16rpx;
  532. border-radius: $radius-full;
  533. flex-shrink: 0;
  534. &.draft {
  535. background: $bg-color-secondary;
  536. .status-text { color: $text-color-muted; }
  537. }
  538. &.pending {
  539. background: $warning-color-bg;
  540. .status-text { color: $warning-color; }
  541. }
  542. &.approved {
  543. background: $success-color-bg;
  544. .status-text { color: $success-color; }
  545. }
  546. &.rejected {
  547. background: $error-color-bg;
  548. .status-text { color: $error-color; }
  549. }
  550. }
  551. .status-text {
  552. font-size: $font-size-xs;
  553. font-weight: 600;
  554. }
  555. .apply-info {
  556. margin-bottom: 12rpx;
  557. }
  558. .info-row {
  559. display: flex;
  560. align-items: baseline;
  561. padding: 6rpx 0;
  562. }
  563. .info-label {
  564. font-size: 22rpx;
  565. color: $text-color-muted;
  566. margin-right: 12rpx;
  567. flex-shrink: 0;
  568. min-width: 80rpx;
  569. }
  570. .info-value {
  571. font-size: 24rpx;
  572. color: $text-color-secondary;
  573. word-break: break-all;
  574. &.price {
  575. color: $primary-color-dark;
  576. font-weight: 600;
  577. }
  578. }
  579. .apply-footer {
  580. display: flex;
  581. align-items: center;
  582. justify-content: space-between;
  583. padding-top: 20rpx;
  584. }
  585. .footer-left {
  586. display: flex;
  587. gap: 16rpx;
  588. }
  589. .action-btn {
  590. padding: 8rpx 24rpx;
  591. border-radius: $radius-full;
  592. text {
  593. font-size: $font-size-xs;
  594. font-weight: 500;
  595. }
  596. &.view {
  597. background: $info-color-bg;
  598. text { color: $info-color; }
  599. }
  600. &.edit {
  601. background: $primary-color-bg;
  602. text { color: $primary-color-dark; }
  603. }
  604. &.delete {
  605. background: $error-color-bg;
  606. text { color: $error-color; }
  607. }
  608. &:active {
  609. opacity: 0.7;
  610. }
  611. }
  612. .empty-state {
  613. display: flex;
  614. flex-direction: column;
  615. align-items: center;
  616. padding: 120rpx 0 80rpx;
  617. }
  618. .empty-icon {
  619. width: 100rpx;
  620. height: 100rpx;
  621. border-radius: 50%;
  622. background: $bg-color-secondary;
  623. margin-bottom: 24rpx;
  624. position: relative;
  625. &::before,
  626. &::after {
  627. content: '';
  628. position: absolute;
  629. left: 50%;
  630. transform: translateX(-50%);
  631. background: $text-color-placeholder;
  632. }
  633. &::before {
  634. top: 30rpx;
  635. width: 32rpx;
  636. height: 4rpx;
  637. border-radius: 2rpx;
  638. }
  639. &::after {
  640. top: 46rpx;
  641. width: 24rpx;
  642. height: 4rpx;
  643. border-radius: 2rpx;
  644. }
  645. }
  646. .empty-title {
  647. font-size: $font-size-base;
  648. font-weight: 500;
  649. color: $text-color-secondary;
  650. margin-bottom: 8rpx;
  651. }
  652. .empty-desc {
  653. font-size: $font-size-sm;
  654. color: $text-color-muted;
  655. margin-bottom: 32rpx;
  656. }
  657. .empty-btn {
  658. padding: 16rpx 48rpx;
  659. background: $primary-color;
  660. border-radius: $radius-full;
  661. &:active { opacity: 0.8; }
  662. }
  663. .empty-btn-text {
  664. font-size: $font-size-base;
  665. color: $text-color-primary;
  666. font-weight: 600;
  667. }
  668. .load-more {
  669. padding: 20rpx 0 40rpx;
  670. text-align: center;
  671. }
  672. .load-more-row {
  673. display: flex;
  674. align-items: center;
  675. justify-content: center;
  676. gap: 10rpx;
  677. }
  678. .load-spinner {
  679. width: 28rpx;
  680. height: 28rpx;
  681. border: 3rpx solid $border-color;
  682. border-top-color: $text-color-muted;
  683. border-radius: 50%;
  684. animation: spin 0.8s linear infinite;
  685. }
  686. @keyframes spin {
  687. to { transform: rotate(360deg); }
  688. }
  689. .load-more-text {
  690. font-size: 22rpx;
  691. color: $text-color-muted;
  692. }
  693. .fab-btn {
  694. position: fixed;
  695. right: 40rpx;
  696. bottom: 140rpx;
  697. width: 100rpx;
  698. height: 100rpx;
  699. background: $primary-color;
  700. border-radius: 50%;
  701. display: flex;
  702. align-items: center;
  703. justify-content: center;
  704. box-shadow: 0 8rpx 24rpx rgba(255, 193, 7, 0.4);
  705. z-index: 100;
  706. &:active {
  707. transform: scale(0.92);
  708. }
  709. }
  710. .fab-icon {
  711. font-size: 52rpx;
  712. color: $text-color-primary;
  713. font-weight: 700;
  714. line-height: 1;
  715. }
  716. </style>