review.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. <template>
  2. <div class="p-2">
  3. <!-- 头部信息 -->
  4. <el-card shadow="hover" class="mb-[10px]">
  5. <div class="flex items-center">
  6. <el-button link type="primary" @click="handleBack">
  7. <el-icon class="mr-1"><ArrowLeft /></el-icon>
  8. 返回
  9. </el-button>
  10. <el-divider direction="vertical" />
  11. <span class="mr-4">客户编号: {{ protocolInfo.customerNo }}</span>
  12. <span class="mr-4">客户名称: {{ protocolInfo.customerName }}</span>
  13. <span>截止日期: {{ protocolInfo.endTime }}</span>
  14. </div>
  15. </el-card>
  16. <!-- 搜索区域 -->
  17. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  18. <div v-show="showSearch" class="mb-[10px]">
  19. <el-card shadow="hover">
  20. <el-form ref="queryFormRef" :model="queryParams" :inline="true">
  21. <el-form-item label="产品编号" prop="productNo">
  22. <el-input v-model="queryParams.productNo" placeholder="请输入产品编号" clearable @keyup.enter="handleQuery" />
  23. </el-form-item>
  24. <el-form-item label="产品名称" prop="productName">
  25. <el-input v-model="queryParams.productName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery" />
  26. </el-form-item>
  27. <el-form-item label="商品品牌" prop="brandName">
  28. <el-select v-model="queryParams.brandName" placeholder="请选择" clearable style="width: 200px">
  29. <el-option v-for="item in brandList" :key="item.id" :label="item.brandName" :value="item.brandName" />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="商品状态" prop="productStatus">
  33. <el-select v-model="queryParams.productStatus" placeholder="请选择" clearable style="width: 200px">
  34. <el-option label="上架" value="1" />
  35. <el-option label="下架" value="0" />
  36. </el-select>
  37. </el-form-item>
  38. <el-form-item label="审核状态" prop="auditStatus">
  39. <el-select v-model="queryParams.auditStatus" placeholder="请选择" clearable style="width: 200px">
  40. <el-option label="待审核" :value="1" />
  41. <el-option label="审核通过" :value="2" />
  42. <el-option label="审核驳回" :value="3" />
  43. </el-select>
  44. </el-form-item>
  45. <el-form-item>
  46. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  47. <el-button type="primary" icon="Refresh" @click="resetQuery">重置</el-button>
  48. </el-form-item>
  49. </el-form>
  50. </el-card>
  51. </div>
  52. </transition>
  53. <!-- 列表区域 -->
  54. <el-card shadow="never">
  55. <template #header>
  56. <el-row :gutter="10" class="mb8" justify="space-between">
  57. <el-col :span="1.5">
  58. <span class="table-title">协议商品详细信息列表</span>
  59. </el-col>
  60. <el-col :span="1.5">
  61. <el-button
  62. type="primary"
  63. icon="Check"
  64. :disabled="selectedProducts.length > 0"
  65. @click="handleBatchAudit"
  66. >批量审核</el-button>
  67. </el-col>
  68. </el-row>
  69. </template>
  70. <el-table v-loading="loading" border :data="productsList" @selection-change="handleTableSelectionChange">
  71. <el-table-column type="selection" width="55" align="center" :selectable="checkSelectableForAudit" />
  72. <el-table-column label="产品编号" align="center" prop="productNo" width="120" />
  73. <el-table-column label="商品图片" align="center" width="100">
  74. <template #default="scope">
  75. <image-preview :src="scope.row.productImage" :width="60" :height="60"/>
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="商品名称" align="center" min-width="180">
  79. <template #default="scope">
  80. <div>
  81. <div class="text-primary">{{ scope.row.itemName }}</div>
  82. <div class="text-gray-400 text-sm">{{ scope.row.brandName }}</div>
  83. </div>
  84. </template>
  85. </el-table-column>
  86. <el-table-column label="产品类别" align="center" prop="categoryName" width="100" />
  87. <el-table-column label="单位" align="center" prop="unitName" width="80" />
  88. <el-table-column label="价格信息" align="center" width="140">
  89. <template #default="scope">
  90. <div class="text-left">
  91. <div>市场价: ¥{{ scope.row.marketPrice || 0 }}</div>
  92. <div class="text-red-500">官网价: ¥{{ scope.row.memberPrice || 0 }}</div>
  93. <div>标准成本: ¥{{ scope.row.purchasingPrice || 0 }}</div>
  94. <div>最低售价: ¥{{ scope.row.minSellingPrice || 0 }}</div>
  95. </div>
  96. </template>
  97. </el-table-column>
  98. <el-table-column label="产品来源" align="center" prop="dataSource" width="100" />
  99. <el-table-column label="状态" align="center" width="80">
  100. <template #default="scope">
  101. <span :class="scope.row.productStatus === '1' ? 'text-green-500' : 'text-red-500'">
  102. {{ scope.row.productStatus === '1' ? '上架' : '下架' }}
  103. </span>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="审核状态" align="center" width="100">
  107. <template #default="scope">
  108. <el-tag v-if="scope.row.auditStatus === 1" type="warning">待审核</el-tag>
  109. <el-tag v-else-if="scope.row.auditStatus === 2" type="success">审核通过</el-tag>
  110. <el-tag v-else-if="scope.row.auditStatus === 3" type="danger">审核驳回</el-tag>
  111. <span v-else>-</span>
  112. </template>
  113. </el-table-column>
  114. <el-table-column label="审核意见" align="center" width="150" show-overflow-tooltip>
  115. <template #default="scope">
  116. <span>{{ scope.row.auditReason || '-' }}</span>
  117. </template>
  118. </el-table-column>
  119. <el-table-column label="协议价" align="center" width="140">
  120. <template #default="scope">
  121. <el-input-number
  122. v-model="scope.row.agreementPrice"
  123. :min="0"
  124. :precision="2"
  125. size="small"
  126. controls-position="right"
  127. @change="handlePriceChange(scope.row)"
  128. />
  129. </template>
  130. </el-table-column>
  131. <el-table-column label="供货价毛利率" align="center" width="120">
  132. <template #default="scope">
  133. {{ calculateMargin(scope.row) }}
  134. </template>
  135. </el-table-column>
  136. <el-table-column label="操作" align="center" fixed="right" width="80">
  137. <template #default="scope">
  138. <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
  139. </template>
  140. </el-table-column>
  141. </el-table>
  142. <pagination
  143. v-show="total > 0"
  144. :total="total"
  145. v-model:page="queryParams.pageNum"
  146. v-model:limit="queryParams.pageSize"
  147. @pagination="getList"
  148. />
  149. </el-card>
  150. <!-- 批量审核对话框 -->
  151. <el-dialog v-model="auditDialog.visible" title="批量审核" width="600px" append-to-body>
  152. <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
  153. <el-form-item label="审核结果" prop="auditStatus">
  154. <el-radio-group v-model="auditForm.auditStatus">
  155. <el-radio :value="2">审核通过</el-radio>
  156. <el-radio :value="3">审核驳回</el-radio>
  157. </el-radio-group>
  158. </el-form-item>
  159. <el-form-item label="审核意见" prop="auditReason">
  160. <el-input v-model="auditForm.auditReason" type="textarea" :rows="4" placeholder="请输入审核意见" />
  161. </el-form-item>
  162. </el-form>
  163. <template #footer>
  164. <div class="dialog-footer">
  165. <el-button @click="auditDialog.visible = false">取消</el-button>
  166. <el-button type="primary" @click="submitBatchAudit">确定</el-button>
  167. </div>
  168. </template>
  169. </el-dialog>
  170. <!-- 添加商品对话框 -->
  171. <el-dialog title="添加商品" v-model="addProductDialog.visible" width="1400px" append-to-body top="5vh">
  172. <div class="add-product-dialog">
  173. <!-- 搜索区域 -->
  174. <el-form :model="addProductQuery" :inline="true" class="mb-4">
  175. <el-form-item>
  176. <el-button type="primary" icon="Plus" @click="handleBatchAdd">加入清单</el-button>
  177. </el-form-item>
  178. <el-form-item label="商品名称:">
  179. <el-input v-model="addProductQuery.itemName" placeholder="商品名称" clearable style="width: 200px" />
  180. </el-form-item>
  181. <el-form-item label="商品编号:">
  182. <el-input v-model="addProductQuery.productNo" placeholder="商品编号" clearable style="width: 200px" />
  183. </el-form-item>
  184. <el-form-item>
  185. <el-button type="primary" icon="Search" @click="handleSearchProducts">搜索</el-button>
  186. </el-form-item>
  187. </el-form>
  188. <!-- 商品列表 -->
  189. <el-table
  190. ref="addProductTableRef"
  191. v-loading="addProductDialog.loading"
  192. :data="addProductDialog.productList"
  193. border
  194. @selection-change="handleSelectionChange"
  195. max-height="500"
  196. >
  197. <el-table-column type="selection" width="55" align="center" :selectable="checkSelectable" />
  198. <el-table-column label="商品编号" align="center" prop="productNo" width="120" />
  199. <el-table-column label="商品图片" align="center" prop="productImageUrl" width="100">
  200. <template #default="scope">
  201. <image-preview :src="scope.row.productImage " :width="60" :height="60"/>
  202. </template>
  203. </el-table-column>
  204. <el-table-column label="商品信息" align="center" min-width="200">
  205. <template #default="scope">
  206. <div class="text-left" style="font-size: 12px;">
  207. <div>{{ scope.row.itemName }}</div>
  208. <div class="text-gray-500">品牌:{{ scope.row.brandName || '-' }}</div>
  209. </div>
  210. </template>
  211. </el-table-column>
  212. <el-table-column label="商品分类" align="center" width="150">
  213. <template #default="scope">
  214. <div class="text-left" style="font-size: 12px;">
  215. <div>{{ scope.row.categoryName || '-' }}</div>
  216. </div>
  217. </template>
  218. </el-table-column>
  219. <el-table-column label="单位" align="center" width="100">
  220. <template #default="scope">
  221. <div class="text-left" style="font-size: 12px;">
  222. <div>单位:{{ scope.row.unitName || '-' }}</div>
  223. <div>起订量:{{ scope.row.minOrderQuantity || 1 }}</div>
  224. </div>
  225. </template>
  226. </el-table-column>
  227. <el-table-column label="价格信息" align="center" width="150">
  228. <template #default="scope">
  229. <div class="text-left" style="font-size: 12px;">
  230. <div>
  231. <span class="text-gray-500">市场价:</span>
  232. <span>¥{{ scope.row.midRangePrice || scope.row.marketPrice || '0.00' }}</span>
  233. </div>
  234. <div>
  235. <span class="text-gray-500">官网价:</span>
  236. <span class="text-red-500">¥{{ scope.row.standardPrice || scope.row.memberPrice || '0.00' }}</span>
  237. </div>
  238. <div>
  239. <span class="text-gray-500">最低价:</span>
  240. <span>¥{{ scope.row.certificatePrice || scope.row.minSellingPrice || '0.00' }}</span>
  241. </div>
  242. </div>
  243. </template>
  244. </el-table-column>
  245. <el-table-column label="状态" align="center" width="100">
  246. <template #default="scope">
  247. <el-tag v-if="isProductAdded(scope.row.id)" type="info">已添加</el-tag>
  248. <el-tag v-else-if="scope.row.productStatus === '1'" type="success">上架</el-tag>
  249. <el-tag v-else type="warning">下架</el-tag>
  250. </template>
  251. </el-table-column>
  252. <el-table-column label="操作" align="center" width="100" fixed="right">
  253. <template #default="scope">
  254. <el-link
  255. v-if="!isProductAdded(scope.row.id)"
  256. type="primary"
  257. :underline="false"
  258. @click="handleAddSingleProduct(scope.row)"
  259. >加入清单</el-link>
  260. <span v-else class="text-gray-400">已添加</span>
  261. </template>
  262. </el-table-column>
  263. </el-table>
  264. <!-- 分页 -->
  265. <pagination
  266. v-show="addProductDialog.productList.length > 0"
  267. v-model:page="addProductQuery.pageNum"
  268. v-model:limit="addProductQuery.pageSize"
  269. v-model:way="addProductQuery.way"
  270. :cursor-mode="true"
  271. :has-more="addProductHasMore"
  272. @pagination="getProductListForAdd"
  273. />
  274. </div>
  275. </el-dialog>
  276. </div>
  277. </template>
  278. <script setup name="ProductManage" lang="ts">
  279. import { listProducts, delProducts, updateProducts, addProducts, getProtocolProductIds, updateProductsPrice, updateProductsAudit } from '@/api/product/products';
  280. import { ProductsQuery, ProductsForm } from '@/api/product/products/types';
  281. import { getInfo } from '@/api/product/protocolInfo';
  282. import { listBrand } from '@/api/product/brand';
  283. import { BrandVO } from '@/api/product/brand/types';
  284. import { BaseVO, BaseQuery } from '@/api/product/base/types';
  285. import { listBase } from '@/api/product/base';
  286. import { ArrowLeft } from '@element-plus/icons-vue';
  287. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  288. const router = useRouter();
  289. const route = useRoute();
  290. // 协议ID
  291. const protocolId = ref<string | number>('');
  292. // 协议编号
  293. const protocolNo = ref<string>('');
  294. // 协议信息
  295. const protocolInfo = ref<any>({
  296. customerNo: '',
  297. customerName: '',
  298. endTime: ''
  299. });
  300. const productsList = ref<BaseVO[]>([]);
  301. const brandList = ref<BrandVO[]>([]);
  302. const loading = ref(true);
  303. const showSearch = ref(true);
  304. const total = ref(0);
  305. const queryFormRef = ref<ElFormInstance>();
  306. const queryParams = ref<ProductsQuery>({
  307. pageNum: 1,
  308. pageSize: 10,
  309. protocolId: route.query.protocolId as string | number,
  310. protocolNo: undefined,
  311. productNo: undefined,
  312. productName: undefined,
  313. brandName: undefined,
  314. productStatus: undefined
  315. });
  316. // 已关联的产品ID列表
  317. const addedProductIds = ref<Set<string | number>>(new Set());
  318. // 添加商品对话框
  319. const addProductDialog = reactive({
  320. visible: false,
  321. loading: false,
  322. productList: [] as BaseVO[],
  323. total: 0
  324. });
  325. // 游标分页相关变量
  326. const addProductHasMore = ref(true);
  327. const addProductPageHistory = ref<Array<{ firstId: string | number; lastId: string | number }>>([]);
  328. // 添加商品查询参数
  329. const addProductQuery = ref<BaseQuery>({
  330. pageNum: 1,
  331. pageSize: 10,
  332. way: undefined,
  333. productNo: undefined,
  334. itemName: undefined,
  335. lastSeenId: undefined
  336. });
  337. // 选中的商品(添加商品弹窗用)
  338. const selectedProducts = ref<BaseVO[]>([]);
  339. const addProductTableRef = ref<any>();
  340. // 列表选中的商品(批量审核用)
  341. const tableSelectedProducts = ref<BaseVO[]>([]);
  342. // 审核对话框
  343. const auditDialog = reactive({
  344. visible: false
  345. });
  346. // 审核表单
  347. const auditFormRef = ref<ElFormInstance>();
  348. const auditForm = ref({
  349. auditStatus: 2,
  350. auditReason: '',
  351. protocolId: route.query.protocolId as string | number
  352. });
  353. // 审核表单验证规则
  354. const auditRules = ref({
  355. auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
  356. auditReason: [{ required: true, message: '请输入审核意见', trigger: 'blur' }]
  357. });
  358. /** 查询协议商品列表 */
  359. const getList = async () => {
  360. loading.value = true;
  361. try {
  362. const res = await listProducts(queryParams.value);
  363. productsList.value = res.rows || [];
  364. total.value = res.total || 0;
  365. } finally {
  366. loading.value = false;
  367. }
  368. };
  369. /** 加载协议信息 */
  370. const loadProtocolInfo = async () => {
  371. if (!protocolId.value) return;
  372. const res = await getInfo(protocolId.value);
  373. if (res.data) {
  374. protocolInfo.value = {
  375. customerNo: res.data.customerNo || '',
  376. customerName: res.data.customerName || '',
  377. endTime: res.data.endTime ? parseTime(res.data.endTime, '{y}/{m}/{d} {h}:{i}:{s}') : ''
  378. };
  379. // 设置协议编号用于查询
  380. protocolNo.value = res.data.protocolNo || '';
  381. queryParams.value.protocolNo = res.data.protocolNo;
  382. }
  383. };
  384. /** 加载品牌列表 */
  385. const loadBrandList = async () => {
  386. const res = await listBrand({ pageNum: 1, pageSize: 1000 });
  387. brandList.value = res.rows || [];
  388. };
  389. /** 加载已关联的产品ID列表 */
  390. const loadAddedProductIds = async () => {
  391. const protocolId = route.query.protocolId as string | number;
  392. try {
  393. const res = await getProtocolProductIds(protocolId);
  394. addedProductIds.value = new Set(res.data || []);
  395. } catch (error) {
  396. console.error('获取已关联产品ID失败:', error);
  397. addedProductIds.value = new Set();
  398. }
  399. };
  400. /** 判断商品是否已添加 */
  401. const isProductAdded = (productId: string | number): boolean => {
  402. return addedProductIds.value.has(String(productId)) || addedProductIds.value.has(Number(productId));
  403. };
  404. /** 检查行是否可选择 */
  405. const checkSelectable = (row: BaseVO): boolean => {
  406. return !isProductAdded(row.id);
  407. };
  408. /** 格式化时间 */
  409. const parseTime = (time: any, pattern?: string) => {
  410. if (!time) return '';
  411. const date = new Date(time);
  412. const fmt = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
  413. const o: Record<string, number> = {
  414. y: date.getFullYear(),
  415. m: date.getMonth() + 1,
  416. d: date.getDate(),
  417. h: date.getHours(),
  418. i: date.getMinutes(),
  419. s: date.getSeconds()
  420. };
  421. return fmt.replace(/{([ymdhis])+}/g, (match, key) => {
  422. const val = o[key];
  423. return val.toString().padStart(2, '0');
  424. });
  425. };
  426. /** 搜索按钮操作 */
  427. const handleQuery = () => {
  428. queryParams.value.pageNum = 1;
  429. getList();
  430. };
  431. /** 重置按钮操作 */
  432. const resetQuery = () => {
  433. queryFormRef.value?.resetFields();
  434. // 保留协议编号
  435. const pNo = queryParams.value.protocolNo;
  436. const protocolId = queryParams.value.protocolId
  437. queryParams.value = {
  438. pageNum: 1,
  439. pageSize: 10,
  440. protocolNo: pNo,
  441. protocolId: protocolId || undefined,
  442. productNo: undefined,
  443. productName: undefined,
  444. brandName: undefined,
  445. productStatus: undefined
  446. };
  447. handleQuery();
  448. };
  449. /** 返回按钮 */
  450. const handleBack = () => {
  451. router.back();
  452. };
  453. /** 添加商品按钮 */
  454. const handleAdd = async () => {
  455. addProductDialog.visible = true;
  456. // 重置查询条件
  457. addProductQuery.value = {
  458. pageNum: 1,
  459. pageSize: 10,
  460. way: undefined,
  461. productNo: undefined,
  462. itemName: undefined,
  463. lastSeenId: undefined
  464. };
  465. // 重置游标分页状态
  466. addProductPageHistory.value = [];
  467. addProductHasMore.value = true;
  468. selectedProducts.value = [];
  469. // 先加载已关联的产品ID
  470. await loadAddedProductIds();
  471. // 再加载商品列表
  472. getProductListForAdd();
  473. };
  474. /** 获取商品列表用于添加 */
  475. const getProductListForAdd = async () => {
  476. addProductDialog.loading = true;
  477. try {
  478. const params = { ...addProductQuery.value };
  479. const currentPageNum = addProductQuery.value.pageNum;
  480. // 第一页不需要游标参数
  481. if (currentPageNum === 1) {
  482. delete params.lastSeenId;
  483. delete params.firstSeenId;
  484. delete params.way;
  485. } else {
  486. // way参数:0=上一页,1=下一页
  487. if (addProductQuery.value.way === 0) {
  488. // 上一页:使用目标页的firstId
  489. const nextPageHistory = addProductPageHistory.value[currentPageNum];
  490. if (nextPageHistory) {
  491. params.firstSeenId = nextPageHistory.firstId;
  492. params.way = 0;
  493. }
  494. } else {
  495. // 下一页:使用前一页的lastId作为lastSeenId
  496. const prevPageHistory = addProductPageHistory.value[currentPageNum - 1];
  497. if (prevPageHistory) {
  498. params.lastSeenId = prevPageHistory.lastId;
  499. params.way = 1;
  500. }
  501. }
  502. }
  503. const res = await listBase(params);
  504. // 兼容两种返回结构
  505. if (res.rows) {
  506. addProductDialog.productList = res.rows;
  507. addProductDialog.total = res.total || 0;
  508. } else if (res.data) {
  509. addProductDialog.productList = Array.isArray(res.data) ? res.data : [];
  510. addProductDialog.total = addProductDialog.productList.length;
  511. } else {
  512. addProductDialog.productList = [];
  513. addProductDialog.total = 0;
  514. }
  515. // 判断是否还有更多数据
  516. addProductHasMore.value = addProductDialog.productList.length === addProductQuery.value.pageSize;
  517. // 记录当前页的第一个id和最后一个id
  518. if (addProductDialog.productList.length > 0) {
  519. const firstItem = addProductDialog.productList[0];
  520. const lastItem = addProductDialog.productList[addProductDialog.productList.length - 1];
  521. // 如果长度小于currentPageNum则创建
  522. if (addProductPageHistory.value.length <= currentPageNum) {
  523. addProductPageHistory.value[currentPageNum] = {
  524. firstId: firstItem.id,
  525. lastId: lastItem.id
  526. };
  527. }
  528. }
  529. } catch (error) {
  530. console.error('获取商品列表失败:', error);
  531. addProductDialog.productList = [];
  532. addProductDialog.total = 0;
  533. } finally {
  534. addProductDialog.loading = false;
  535. }
  536. };
  537. /** 搜索商品 */
  538. const handleSearchProducts = () => {
  539. addProductQuery.value.pageNum = 1;
  540. addProductQuery.value.lastSeenId = undefined;
  541. addProductPageHistory.value = []; // 重置页面历史
  542. addProductHasMore.value = true;
  543. getProductListForAdd();
  544. };
  545. /** 选择变化(添加商品弹窗用) */
  546. const handleSelectionChange = (selection: BaseVO[]) => {
  547. selectedProducts.value = selection;
  548. };
  549. /** 表格选择变化(批量审核用) */
  550. const handleTableSelectionChange = (selection: BaseVO[]) => {
  551. tableSelectedProducts.value = selection;
  552. };
  553. /** 检查行是否可选择(批量审核用,只有待审核状态才能选择) */
  554. const checkSelectableForAudit = (row: BaseVO): boolean => {
  555. return row.auditStatus === 1;
  556. };
  557. /** 批量审核按钮 */
  558. const handleBatchAudit = () => {
  559. if (tableSelectedProducts.value.length === 0) {
  560. proxy?.$modal.msgWarning('请先选择要审核的商品');
  561. return;
  562. }
  563. auditDialog.visible = true;
  564. auditForm.value = {
  565. protocolId: route.query.protocolId as string | number,
  566. auditStatus: 2,
  567. auditReason: ''
  568. };
  569. };
  570. /** 提交批量审核 */
  571. const submitBatchAudit = async () => {
  572. await auditFormRef.value?.validate();
  573. try {
  574. const data = tableSelectedProducts.value.map(item => ({
  575. id: item.id,
  576. productId: item.id,
  577. protocolId: auditForm.value.protocolId,
  578. auditStatus: auditForm.value.auditStatus,
  579. auditReason: auditForm.value.auditReason
  580. }));
  581. await updateProductsAudit(data);
  582. proxy?.$modal.msgSuccess(auditForm.value.auditStatus === 2 ? '审核通过' : '审核驳回');
  583. auditDialog.visible = false;
  584. await getList();
  585. } catch (error) {
  586. console.error('批量审核失败:', error);
  587. proxy?.$modal.msgError('审核失败');
  588. }
  589. };
  590. /** 批量加入清单 */
  591. const handleBatchAdd = async () => {
  592. if (selectedProducts.value.length === 0) {
  593. proxy?.$modal.msgWarning('请先选择要添加的商品');
  594. return;
  595. }
  596. try {
  597. // 逐个添加商品
  598. for (const product of selectedProducts.value) {
  599. const data: ProductsForm = {
  600. protocolNo: protocolNo.value,
  601. protocolId: route.query.protocolId as string | number,
  602. productNo: product.productNo,
  603. productId: product.id,
  604. agreementPrice: product.standardPrice || product.midRangePrice || product.memberPrice || 0,
  605. customerId: protocolInfo.value.customerId,
  606. dataSource: product.dataSource
  607. };
  608. await addProducts(data);
  609. }
  610. proxy?.$modal.msgSuccess(`成功添加 ${selectedProducts.value.length} 个商品`);
  611. addProductDialog.visible = false;
  612. // 清空选中项
  613. selectedProducts.value = [];
  614. if (addProductTableRef.value) {
  615. addProductTableRef.value.clearSelection();
  616. }
  617. // 刷新列表
  618. await loadAddedProductIds();
  619. await getList();
  620. } catch (error) {
  621. console.error('添加商品失败:', error);
  622. proxy?.$modal.msgError('添加商品失败');
  623. }
  624. };
  625. /** 添加单个商品 */
  626. const handleAddSingleProduct = async (row: BaseVO) => {
  627. try {
  628. const data: ProductsForm = {
  629. protocolNo: protocolNo.value,
  630. protocolId: route.query.protocolId as string | number,
  631. productNo: row.productNo,
  632. productId: row.id,
  633. agreementPrice: row.standardPrice || row.midRangePrice || row.memberPrice || 0,
  634. customerId: protocolInfo.value.customerId,
  635. dataSource: row.dataSource
  636. };
  637. await addProducts(data);
  638. proxy?.$modal.msgSuccess('添加成功');
  639. // 更新已添加的产品ID列表
  640. addedProductIds.value.add(row.id);
  641. // 刷新主列表
  642. await getList();
  643. } catch (error) {
  644. console.error('添加商品失败:', error);
  645. proxy?.$modal.msgError('添加商品失败');
  646. }
  647. };
  648. /** 导入商品按钮 */
  649. const handleImport = () => {
  650. proxy?.$modal.msgWarning('导入商品功能待实现');
  651. };
  652. /** 导出商品按钮 */
  653. const handleExport = () => {
  654. proxy?.download('product/products/export', {
  655. ...queryParams.value
  656. }, `products_${new Date().getTime()}.xlsx`);
  657. };
  658. /** 删除按钮操作 */
  659. const handleDelete = async (row: any) => {
  660. await proxy?.$modal.confirm('是否确认删除该商品?');
  661. await delProducts(row.id);
  662. proxy?.$modal.msgSuccess('删除成功');
  663. await getList();
  664. };
  665. /** 协议供货价变更 */
  666. const handlePriceChange = async (row: any) => {
  667. try {
  668. await updateProductsPrice({
  669. agreementPrice: row.agreementPrice,
  670. productId: row.id,
  671. protocolId: route.query.protocolId as string | number
  672. });
  673. proxy?.$modal.msgSuccess('价格更新成功');
  674. } catch (error) {
  675. proxy?.$modal.msgError('价格更新失败');
  676. }
  677. };
  678. /** 计算供货价毛利率 */
  679. const calculateMargin = (row: any) => {
  680. if (!row.agreementPrice || !row.purchasingPrice || row.purchasingPrice === 0) {
  681. return '-';
  682. }
  683. const margin = ((row.agreementPrice - row.purchasingPrice) / row.agreementPrice * 100).toFixed(2)+'%';
  684. return margin;
  685. };
  686. onMounted(() => {
  687. // 从路由参数获取协议ID
  688. protocolId.value = route.query.protocolId as string || route.params.id as string || '';
  689. if (protocolId.value) {
  690. loadProtocolInfo();
  691. loadBrandList();
  692. getList();
  693. } else {
  694. proxy?.$modal.msgError('缺少协议ID参数');
  695. router.back();
  696. }
  697. });
  698. </script>
  699. <style scoped>
  700. .text-primary {
  701. color: #409eff;
  702. }
  703. .text-gray-400 {
  704. color: #909399;
  705. }
  706. .text-gray-500 {
  707. color: #909399;
  708. }
  709. .text-red-500 {
  710. color: #f56c6c;
  711. }
  712. .text-green-500 {
  713. color: #67c23a;
  714. }
  715. .text-sm {
  716. font-size: 12px;
  717. }
  718. .add-product-dialog {
  719. :deep(.el-form--inline .el-form-item) {
  720. margin-right: 10px;
  721. }
  722. }
  723. </style>