index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <template>
  2. <div class="p-2">
  3. <!-- 入池清单信息 -->
  4. <el-card shadow="hover" class="mb-[10px]" v-loading="auditInfoLoading">
  5. <template #header>
  6. <span class="font-bold">入池清单信息</span>
  7. </template>
  8. <el-descriptions :column="3" border>
  9. <el-descriptions-item label="编号">{{ auditInfo?.id }}</el-descriptions-item>
  10. <el-descriptions-item label="产品池名称">{{ auditInfo?.poolName || auditInfo?.name }}</el-descriptions-item>
  11. <el-descriptions-item label="申请时间">{{ auditInfo?.createTime }}</el-descriptions-item>
  12. <el-descriptions-item label="审核时间">{{ auditInfo?.auditTime || '-' }}</el-descriptions-item>
  13. <el-descriptions-item label="状态">
  14. <el-tag :type="getStatusTagType(auditInfo?.productReviewStatus)">{{ getStatusLabel(auditInfo?.productReviewStatus) }}</el-tag>
  15. </el-descriptions-item>
  16. <el-descriptions-item label="创建人">{{ auditInfo?.createByName }}</el-descriptions-item>
  17. <el-descriptions-item label="审核人">{{ auditInfo?.auditByName || '-' }}</el-descriptions-item>
  18. <el-descriptions-item label="备注">{{ auditInfo?.remark || '-' }}</el-descriptions-item>
  19. <el-descriptions-item label="驳回意见">{{ auditInfo?.reviewReason || '-' }}</el-descriptions-item>
  20. </el-descriptions>
  21. </el-card>
  22. <!-- 入池清单 -->
  23. <el-card shadow="never">
  24. <template #header>
  25. <span class="font-bold">入池清单</span>
  26. </template>
  27. <div class="mb-4" v-if="!auditInfoLoading && !isReadOnly">
  28. <el-button type="danger" icon="Remove" @click="handleRemoveFromPoolListBatch">移出入池清单</el-button>
  29. </div>
  30. <el-table
  31. ref="poolTableRef"
  32. v-loading="listLoading"
  33. :data="poolList"
  34. border
  35. @selection-change="handlePoolSelectionChange"
  36. >
  37. <el-table-column type="selection" width="55" align="center" />
  38. <el-table-column type="index" label="序号" width="60" align="center" />
  39. <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
  40. <el-table-column label="商品图片" align="center" prop="productImage" width="100">
  41. <template #default="scope">
  42. <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
  43. </template>
  44. </el-table-column>
  45. <el-table-column label="商品信息" align="center" min-width="180">
  46. <template #default="scope">
  47. <div class="text-left" style="font-size: 12px;">
  48. <div>{{ scope.row.itemName }}</div>
  49. <div class="text-gray-500">品牌:{{ scope.row.brandName || '-' }}</div>
  50. </div>
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="单位" align="center" prop="unitName" width="80" />
  54. <el-table-column label="SKU价格" align="center" width="150">
  55. <template #default="scope">
  56. <div class="text-left" style="font-size: 12px;">
  57. <div>
  58. <span class="text-gray-500">市场价:</span>
  59. <span class="text-red-500">¥{{ scope.row.marketPrice || '0.00' }}</span>
  60. </div>
  61. <div>
  62. <span class="text-gray-500">官网价:</span>
  63. <span class="text-red-500">¥{{ scope.row.memberPrice || '0.00' }}</span>
  64. </div>
  65. </div>
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="成本数据" align="center" width="130">
  69. <template #default="scope">
  70. <div class="text-left" style="font-size: 12px;">
  71. <div>
  72. <span class="text-gray-500">采购价:</span>
  73. <span>¥{{ scope.row.purchasePrice || '0.00' }}</span>
  74. </div>
  75. </div>
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="项目/平台价" align="center" width="100">
  79. <template #default="scope">
  80. <span class="text-red-500">¥{{ scope.row.agreementPrice ?? scope.row.minSellingPrice ?? '0.00' }}</span>
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="商品状态" align="center" width="80">
  84. <template #default="scope">
  85. <el-tag v-if="scope.row.productStatus === '1'" type="success">上架</el-tag>
  86. <el-tag v-else type="warning">下架</el-tag>
  87. </template>
  88. </el-table-column>
  89. <el-table-column label="创建供应商" align="center" width="100">
  90. <template #default="scope">
  91. <span>{{ getSupplierName(scope.row.supplier) }}</span>
  92. </template>
  93. </el-table-column>
  94. <el-table-column v-if="!auditInfoLoading && !isReadOnly" label="操作" align="center" width="80" fixed="right">
  95. <template #default="scope">
  96. <el-link type="danger" :underline="false" @click="handleRemoveFromPoolList(scope.row)">移除</el-link>
  97. </template>
  98. </el-table-column>
  99. </el-table>
  100. <pagination
  101. v-show="listTotal > 0"
  102. :total="listTotal"
  103. v-model:page="listQuery.pageNum"
  104. v-model:limit="listQuery.pageSize"
  105. @pagination="getPoolList"
  106. />
  107. </el-card>
  108. <!-- 底部操作按钮 -->
  109. <div v-if="!auditInfoLoading && !isReadOnly" class="fixed bottom-0 left-0 right-0 bg-white border-t p-4 flex justify-center gap-4 z-10">
  110. <el-button type="danger" size="large" @click="handleReject">驳 回</el-button>
  111. <el-button type="primary" size="large" @click="handlePass">通 过</el-button>
  112. </div>
  113. <!-- 驳回原因对话框 -->
  114. <el-dialog title="驳回原因" v-model="rejectDialog.visible" width="500px" append-to-body>
  115. <el-form :model="rejectDialog.form" label-width="80px">
  116. <el-form-item label="驳回原因" required>
  117. <el-input
  118. v-model="rejectDialog.form.reason"
  119. type="textarea"
  120. :rows="4"
  121. placeholder="请输入驳回原因"
  122. />
  123. </el-form-item>
  124. </el-form>
  125. <template #footer>
  126. <el-button @click="rejectDialog.visible = false">取 消</el-button>
  127. <el-button type="primary" @click="confirmReject">确 定</el-button>
  128. </template>
  129. </el-dialog>
  130. </div>
  131. </template>
  132. <script setup name="PoolAuditReview" lang="ts">
  133. import { useRouter, useRoute } from 'vue-router';
  134. import { getPoolAudit, updatePoolAudit, getPoolAuditProductPage, batchAudit } from '@/api/product/poolAudit';
  135. import { PoolAuditVO } from '@/api/product/poolAudit/types';
  136. import { delPoolLinkAudit } from '@/api/product/poolLinkAudit';
  137. import { PoolLinkVO } from '@/api/product/poolLink/types';
  138. import { listInfo } from '@/api/customer/supplierInfo';
  139. import { InfoVO } from '@/api/customer/supplierInfo/types';
  140. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  141. const router = useRouter();
  142. const route = useRoute();
  143. const poolAuditId = computed(() => (route.params.id || route.query.id) as string | number);
  144. // 审核信息
  145. const auditInfoLoading = ref(false);
  146. const auditInfo = ref<(PoolAuditVO & { createTime?: string; auditTime?: string }) | undefined>();
  147. // 入池清单
  148. const listLoading = ref(false);
  149. const poolList = ref<PoolLinkVO[]>([]);
  150. const listTotal = ref(0);
  151. const listQuery = ref({
  152. pageNum: 1,
  153. pageSize: 10
  154. });
  155. const selectedPoolProducts = ref<PoolLinkVO[]>([]);
  156. const poolTableRef = ref<any>();
  157. // 供应商
  158. const supplierOptions = ref<InfoVO[]>([]);
  159. /** 是否只读(审核通过或已驳回状态) */
  160. const isReadOnly = computed(() => {
  161. const status = auditInfo.value?.productReviewStatus;
  162. return status === '2' || status === '3';
  163. });
  164. // 驳回对话框
  165. const rejectDialog = reactive({
  166. visible: false,
  167. form: {
  168. reason: ''
  169. }
  170. });
  171. /** 获取审核状态标签文字 */
  172. const getStatusLabel = (status?: string): string => {
  173. const map: Record<string, string> = { '0': '待提交', '1': '待审核', '2': '审核通过', '3': '审核驳回' };
  174. return status !== undefined ? (map[status] || status) : '-';
  175. };
  176. /** 获取审核状态标签类型 */
  177. const getStatusTagType = (status?: string): 'success' | 'warning' | 'info' | 'danger' | 'primary' => {
  178. const map: Record<string, 'success' | 'warning' | 'info' | 'danger' | 'primary'> = {
  179. '0': 'info', '1': 'warning', '2': 'success', '3': 'danger'
  180. };
  181. return status !== undefined ? (map[status] || 'primary') : 'primary';
  182. };
  183. /** 获取供应商名称 */
  184. const getSupplierName = (supplierId: string | number | undefined): string => {
  185. if (!supplierId) return '-';
  186. const supplier = supplierOptions.value.find(item => item.id === supplierId);
  187. return supplier?.enterpriseName || supplier?.shortName || '-';
  188. };
  189. /** 加载审核信息 */
  190. const loadAuditInfo = async () => {
  191. if (!poolAuditId.value) return;
  192. auditInfoLoading.value = true;
  193. try {
  194. const res = await getPoolAudit(poolAuditId.value);
  195. auditInfo.value = res.data as any;
  196. } catch (error) {
  197. console.error('加载审核信息失败:', error);
  198. } finally {
  199. auditInfoLoading.value = false;
  200. }
  201. };
  202. /** 获取供应商列表 */
  203. const getSupplierList = async () => {
  204. try {
  205. const res = await listInfo();
  206. supplierOptions.value = res.data || res.rows || [];
  207. } catch (error) {
  208. console.error('获取供应商列表失败:', error);
  209. }
  210. };
  211. /** 获取入池清单列表 */
  212. const getPoolList = async () => {
  213. listLoading.value = true;
  214. try {
  215. const params = {
  216. pageNum: listQuery.value.pageNum,
  217. pageSize: listQuery.value.pageSize,
  218. poolAuditId: poolAuditId.value
  219. };
  220. const res = await getPoolAuditProductPage(params);
  221. poolList.value = (res.rows || res.data || []) as any;
  222. listTotal.value = res.total || 0;
  223. } catch (error) {
  224. console.error('获取入池清单列表失败:', error);
  225. poolList.value = [];
  226. listTotal.value = 0;
  227. } finally {
  228. listLoading.value = false;
  229. }
  230. };
  231. /** 入池清单选择变化 */
  232. const handlePoolSelectionChange = (selection: PoolLinkVO[]) => {
  233. selectedPoolProducts.value = selection;
  234. };
  235. /** 批量移出入池清单 */
  236. const handleRemoveFromPoolListBatch = async () => {
  237. if (selectedPoolProducts.value.length === 0) {
  238. proxy?.$modal.msgWarning('请先选择要移出的商品');
  239. return;
  240. }
  241. await proxy?.$modal.confirm(`确认要将 ${selectedPoolProducts.value.length} 个商品移出入池清单吗?`);
  242. try {
  243. const promises = selectedPoolProducts.value.map(item =>
  244. delPoolLinkAudit((item as any).poolAuditProductId)
  245. );
  246. await Promise.all(promises);
  247. proxy?.$modal.msgSuccess('移出成功');
  248. selectedPoolProducts.value = [];
  249. if (poolTableRef.value) {
  250. poolTableRef.value.clearSelection();
  251. }
  252. await getPoolList();
  253. } catch (error) {
  254. console.error('移出入池清单失败:', error);
  255. proxy?.$modal.msgError('移出入池清单失败');
  256. }
  257. };
  258. /** 从入池清单移除单个 */
  259. const handleRemoveFromPoolList = async (row: PoolLinkVO) => {
  260. await proxy?.$modal.confirm('确认要从入池清单移除该商品吗?');
  261. try {
  262. await delPoolLinkAudit((row as any).poolAuditProductId);
  263. proxy?.$modal.msgSuccess('移除成功');
  264. await getPoolList();
  265. } catch (error) {
  266. console.error('移除失败:', error);
  267. proxy?.$modal.msgError('移除失败');
  268. }
  269. };
  270. /** 驳回 */
  271. const handleReject = () => {
  272. rejectDialog.form.reason = '';
  273. rejectDialog.visible = true;
  274. };
  275. /** 确认驳回 */
  276. const confirmReject = async () => {
  277. if (!rejectDialog.form.reason?.trim()) {
  278. proxy?.$modal.msgWarning('请输入驳回原因');
  279. return;
  280. }
  281. try {
  282. await updatePoolAudit({
  283. id: auditInfo.value?.id,
  284. productReviewStatus: '3',
  285. reviewReason: rejectDialog.form.reason
  286. });
  287. proxy?.$modal.msgSuccess('驳回成功');
  288. rejectDialog.visible = false;
  289. router.back();
  290. } catch (error) {
  291. console.error('驳回失败:', error);
  292. }
  293. };
  294. /** 通过 */
  295. const handlePass = async () => {
  296. if (poolList.value.length === 0) {
  297. proxy?.$modal.msgWarning('入池清单为空,请先将商品加入入池清单');
  298. return;
  299. }
  300. await proxy?.$modal.confirm('确认审核通过该入池申请吗?');
  301. try {
  302. // 获取入池清单中所有商品的productId
  303. const productIds = poolList.value.map(item => item.productId);
  304. // 调用批量审核接口
  305. await batchAudit({
  306. poolAuditId: poolAuditId.value,
  307. productIds: productIds,
  308. auditStatus: '2' // 审核通过
  309. });
  310. proxy?.$modal.msgSuccess('审核通过');
  311. router.back();
  312. } catch (error) {
  313. console.error('审核通过失败:', error);
  314. }
  315. };
  316. onMounted(() => {
  317. loadAuditInfo();
  318. getSupplierList();
  319. getPoolList();
  320. });
  321. </script>
  322. <style scoped lang="scss">
  323. .p-2 {
  324. padding-bottom: 80px; // 为底部固定按钮留出空间
  325. }
  326. </style>