Product.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. title="添加商品报价"
  5. width="90%"
  6. :close-on-click-modal="false"
  7. @close="handleClose"
  8. class="product-dialog"
  9. :modal-append-to-body="false"
  10. :append-to-body="true"
  11. >
  12. <div class="dialog-content">
  13. <!-- 搜索表单区域 - 固定 -->
  14. <div class="search-container">
  15. <el-form ref="queryFormRef" :model="queryParams" label-width="80px">
  16. <el-row :gutter="16" class="first-row">
  17. <el-col :span="6"> <!-- 调整为6列,均分24列,避免宽度不足 -->
  18. <el-form-item label="产品编号:" prop="productNo">
  19. <el-input v-model="queryParams.productNo" placeholder="请输入产品编号" clearable @keyup.enter="handleQuery" />
  20. </el-form-item>
  21. </el-col>
  22. <el-col :span="6">
  23. <el-form-item label="产品名称:" prop="itemName">
  24. <el-input v-model="queryParams.itemName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery" />
  25. </el-form-item>
  26. </el-col>
  27. <el-col :span="6">
  28. <el-form-item label="品牌名称:" prop="brandName">
  29. <el-input v-model="queryParams.brandName" placeholder="请选择" clearable @keyup.enter="handleQuery" />
  30. </el-form-item>
  31. </el-col>
  32. <el-col :span="6">
  33. <el-form-item label="上架状态:" prop="productStatus">
  34. <el-select v-model="queryParams.productStatus" placeholder="请选择" clearable style="width: 100%">
  35. <el-option label="已上架" :value="1" />
  36. <el-option label="下架" :value="0" />
  37. <el-option label="上架中" :value="2" />
  38. </el-select>
  39. </el-form-item>
  40. </el-col>
  41. </el-row>
  42. <el-row :gutter="16" class="second-row">
  43. <el-col :span="24" style="text-align: left;">
  44. <el-button type="primary" icon="Plus" @click="handleBatchAddToList">批量加入清单</el-button>
  45. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  46. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  47. </el-col>
  48. </el-row>
  49. </el-form>
  50. </div>
  51. <!-- 表格区域 - 可滚动 -->
  52. <div class="table-wrapper">
  53. <el-table
  54. v-loading="loading"
  55. border
  56. :data="baseList"
  57. @selection-change="handleSelectionChange"
  58. max-height="calc(70vh - 200px)"
  59. >
  60. <el-table-column type="selection" width="50" align="center" />
  61. <el-table-column label="产品编号" align="center" prop="productNo" width="110" />
  62. <el-table-column label="产品图片" align="center" prop="productImage" width="90">
  63. <template #default="scope">
  64. <image-preview :src="scope.row.productImage" :width="60" :height="60" />
  65. </template>
  66. </el-table-column>
  67. <el-table-column label="产品名称" align="center" prop="itemName" width="180" show-overflow-tooltip />
  68. <el-table-column label="产品类型" align="center" prop="categoryName" width="110" />
  69. <el-table-column label="品牌" align="center" prop="brandName" width="100" />
  70. <el-table-column label="单位" align="center" prop="unitName" width="70" />
  71. <el-table-column label="市场价" align="center" width="90">
  72. <template #default="scope">
  73. <span>¥{{ scope.row.marketPrice || '0.00' }}</span>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="平台售价" align="center" width="90">
  77. <template #default="scope">
  78. <span>¥{{ scope.row.memberPrice || '0.00' }}</span>
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="起订量" align="center" prop="minOrderQuantity" width="70" />
  82. <el-table-column label="上架状态" align="center" prop="productStatus" width="90">
  83. <template #default="scope">
  84. <el-tag v-if="scope.row.productStatus === 1" type="success">已上架</el-tag>
  85. <el-tag v-else-if="scope.row.productStatus === 0" type="warning">下架</el-tag>
  86. <el-tag v-else-if="scope.row.productStatus === 2" type="info">上架中</el-tag>
  87. <el-tag v-else type="info">未知</el-tag>
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="操作" align="center" width="100" fixed="right"> <!-- 固定右侧,避免空白 -->
  91. <template #default="scope">
  92. <el-button link type="primary" @click="handleAddToList(scope.row)">加入清单</el-button>
  93. </template>
  94. </el-table-column>
  95. </el-table>
  96. <!-- 分页 -->
  97. <div class="pagination-container">
  98. <Pagination
  99. v-model:page="queryParams.pageNum"
  100. v-model:limit="queryParams.pageSize"
  101. v-model:way="queryParams.way"
  102. :cursor-mode="true"
  103. :has-more="hasMore"
  104. :total="total"
  105. @pagination="getList"
  106. />
  107. </div>
  108. </div>
  109. </div>
  110. </el-dialog>
  111. </template>
  112. <script setup name="Base" lang="ts">
  113. // 补充必要的导入
  114. import { getCurrentInstance, ComponentInternalInstance, computed, ref, reactive, onMounted } from 'vue';
  115. import type { ElFormInstance } from 'element-plus';
  116. // Props 定义
  117. interface Props {
  118. visible: boolean;
  119. modelValue: any[];
  120. }
  121. const props = withDefaults(defineProps<Props>(), {
  122. visible: false,
  123. modelValue: () => []
  124. });
  125. // Emits 定义
  126. const emit = defineEmits<{
  127. 'update:visible': [value: boolean];
  128. 'update:modelValue': [value: any[]];
  129. 'confirm': [selectedProducts: any[]];
  130. }>();
  131. // 控制 dialog 显示
  132. const dialogVisible = computed({
  133. get: () => props.visible,
  134. set: (val) => emit('update:visible', val)
  135. });
  136. // 关闭对话框
  137. const handleClose = () => {
  138. emit('update:visible', false);
  139. };
  140. // 模拟接口(如果实际项目有,可替换)
  141. import { listBase, categoryTree } from '@/api/product/base';
  142. import { BaseVO, BaseQuery, BaseForm } from '@/api/product/base/types';
  143. import { categoryTreeVO } from '@/api/product/category/types';
  144. import Pagination from '@/components/Pagination/index.vue';
  145. // 类型补充
  146. interface PageData<F, Q> {
  147. form: F;
  148. queryParams: Q;
  149. rules: Record<string, any>;
  150. }
  151. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  152. const baseList = ref<BaseVO[]>([]);
  153. const loading = ref(true);
  154. const showSearch = ref(true);
  155. const ids = ref<Array<string | number>>([]);
  156. const single = ref(true);
  157. const multiple = ref(true);
  158. const total = ref(0);
  159. const categoryOptions = ref<categoryTreeVO[]>([]);
  160. const hasMore = ref(true);
  161. const pageHistory = ref([]);
  162. const queryFormRef = ref<ElFormInstance>();
  163. const initFormData: BaseForm = {
  164. id: undefined,
  165. productNo: undefined,
  166. itemName: undefined,
  167. brandId: undefined,
  168. topCategoryId: undefined,
  169. mediumCategoryId: undefined,
  170. bottomCategoryId: undefined,
  171. unitId: undefined,
  172. productImage: undefined,
  173. isSelf: undefined,
  174. productReviewStatus: undefined,
  175. homeRecommended: undefined,
  176. categoryRecommendation: undefined,
  177. cartRecommendation: undefined,
  178. recommendedProductOrder: undefined,
  179. isPopular: undefined,
  180. isNew: undefined,
  181. productStatus: undefined,
  182. remark: undefined
  183. };
  184. const data = reactive<PageData<BaseForm, BaseQuery>>({
  185. form: { ...initFormData },
  186. queryParams: {
  187. pageNum: 1,
  188. pageSize: 10,
  189. productNo: undefined,
  190. itemName: undefined,
  191. brandName: undefined,
  192. productTag: undefined,
  193. purchaseNature: undefined,
  194. supplierType: undefined,
  195. supplierNature: undefined,
  196. projectOrg: undefined,
  197. topCategoryId: undefined,
  198. mediumCategoryId: undefined,
  199. bottomCategoryId: undefined,
  200. isSelf: undefined,
  201. productReviewStatus: undefined,
  202. productStatus: undefined,
  203. lastSeenId: undefined,
  204. way: undefined,
  205. params: {}
  206. },
  207. rules: {
  208. productNo: [{ required: true, message: '产品编号不能为空', trigger: 'blur' }],
  209. itemName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
  210. brandId: [{ required: true, message: '品牌id不能为空', trigger: 'blur' }],
  211. topCategoryId: [{ required: true, message: '顶级分类id不能为空', trigger: 'blur' }],
  212. mediumCategoryId: [{ required: true, message: '中级分类id不能为空', trigger: 'blur' }],
  213. bottomCategoryId: [{ required: true, message: '底层分类id不能为空', trigger: 'blur' }],
  214. unitId: [{ required: true, message: '单位id不能为空', trigger: 'blur' }],
  215. productImage: [{ required: true, message: '产品图片URL不能为空', trigger: 'blur' }],
  216. productReviewStatus: [{ required: true, message: '产品审核状态不能为空', trigger: 'change' }],
  217. homeRecommended: [{ required: true, message: '首页推荐不能为空', trigger: 'blur' }],
  218. categoryRecommendation: [{ required: true, message: '分类推荐不能为空', trigger: 'blur' }],
  219. cartRecommendation: [{ required: true, message: '购物车推荐不能为空', trigger: 'blur' }],
  220. recommendedProductOrder: [{ required: true, message: '推荐产品顺序不能为空', trigger: 'blur' }],
  221. isPopular: [{ required: true, message: '是否热门不能为空', trigger: 'blur' }],
  222. isNew: [{ required: true, message: '是否新品不能为空', trigger: 'blur' }],
  223. remark: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
  224. }
  225. });
  226. const { queryParams } = toRefs(data);
  227. /** 查询产品基础信息列表 */
  228. const getList = async () => {
  229. loading.value = true;
  230. try {
  231. const params = { ...queryParams.value };
  232. const currentPageNum = queryParams.value.pageNum;
  233. if (currentPageNum === 1) {
  234. delete params.lastSeenId;
  235. delete params.way;
  236. } else {
  237. if (queryParams.value.way === 0) {
  238. const nextPageHistory = pageHistory.value[currentPageNum];
  239. if (nextPageHistory) {
  240. params.firstSeenId = nextPageHistory.firstId;
  241. params.way = 0;
  242. }
  243. } else {
  244. const prevPageHistory = pageHistory.value[currentPageNum - 1];
  245. if (prevPageHistory) {
  246. params.lastSeenId = prevPageHistory.lastId;
  247. params.way = 1;
  248. }
  249. }
  250. }
  251. const res = await listBase(params);
  252. baseList.value = res.rows || [];
  253. hasMore.value = baseList.value.length === queryParams.value.pageSize;
  254. if (baseList.value.length > 0) {
  255. const firstItem = baseList.value[0];
  256. const lastItem = baseList.value[baseList.value.length - 1];
  257. if (pageHistory.value.length <= currentPageNum) {
  258. pageHistory.value[currentPageNum] = {
  259. firstId: firstItem.id,
  260. lastId: lastItem.id
  261. };
  262. }
  263. }
  264. total.value = res.total || 0;
  265. } catch (error) {
  266. console.error('获取列表失败:', error);
  267. } finally {
  268. loading.value = false;
  269. }
  270. };
  271. /** 搜索按钮操作 */
  272. const handleQuery = () => {
  273. queryParams.value = {
  274. ...queryParams.value,
  275. pageNum: 1,
  276. productNo: queryParams.value.productNo,
  277. itemName: queryParams.value.itemName,
  278. brandName: queryParams.value.brandName,
  279. bottomCategoryId: queryParams.value.bottomCategoryId,
  280. isSelf: queryParams.value.isSelf,
  281. productReviewStatus: queryParams.value.productReviewStatus,
  282. productStatus: queryParams.value.productStatus,
  283. lastSeenId: undefined
  284. };
  285. pageHistory.value = [];
  286. getList();
  287. };
  288. /** 重置按钮操作 */
  289. const resetQuery = () => {
  290. queryFormRef.value?.resetFields();
  291. queryParams.value.lastSeenId = undefined;
  292. pageHistory.value = [];
  293. handleQuery();
  294. };
  295. /** 多选框选中数据 */
  296. const handleSelectionChange = (selection: BaseVO[]) => {
  297. ids.value = selection.map((item) => item.id);
  298. single.value = selection.length != 1;
  299. multiple.value = !selection.length;
  300. };
  301. /** 加入清单 */
  302. const handleAddToList = (row: BaseVO) => {
  303. const existsInModelValue = props.modelValue.some(item => item.productId === row.id);
  304. if (existsInModelValue) {
  305. proxy?.$modal.msgWarning('该商品已在清单中');
  306. return;
  307. }
  308. const newProduct = {
  309. productId: row.id,
  310. productCode: row.productNo,
  311. productImage: row.productImage,
  312. productName: row.itemName,
  313. productType: row.categoryName,
  314. brand: row.brandName,
  315. unit: row.unitName,
  316. basePrice: row.purchasingPrice || 0,
  317. marketPrice: row.marketPrice || 0,
  318. platformPrice: row.memberPrice || 0,
  319. offerPrice: '',
  320. supplyCycle: '',
  321. upPrice: Number(row.productStatus) === 1 ? '上架' : '下架'
  322. };
  323. emit('update:modelValue', [...props.modelValue, newProduct]);
  324. proxy?.$modal.msgSuccess('添加成功');
  325. emit('update:visible', false);
  326. };
  327. /** 批量加入清单 */
  328. const handleBatchAddToList = () => {
  329. const selectedProducts = baseList.value.filter(item => ids.value.includes(item.id));
  330. if (selectedProducts.length === 0) {
  331. proxy?.$modal.msgWarning('请选择要添加的商品');
  332. return;
  333. }
  334. const newProducts: any[] = [];
  335. selectedProducts.forEach(row => {
  336. const exists = props.modelValue.some(item => item.productId === row.id);
  337. if (!exists) {
  338. newProducts.push({
  339. productId: row.id,
  340. productCode: row.productNo,
  341. productImage: row.productImage,
  342. productName: row.itemName,
  343. productType: row.categoryName,
  344. brand: row.brandName,
  345. unit: row.unitName,
  346. basePrice: row.purchasingPrice || 0,
  347. marketPrice: row.marketPrice || 0,
  348. platformPrice: row.memberPrice || 0,
  349. offerPrice: '',
  350. supplyCycle: '',
  351. upPrice: Number(row.productStatus) === 1 ? '上架' : '下架'
  352. });
  353. }
  354. });
  355. if (newProducts.length === 0) {
  356. proxy?.$modal.msgWarning('所选商品已在清单中');
  357. return;
  358. }
  359. emit('update:modelValue', [...props.modelValue, ...newProducts]);
  360. proxy?.$modal.msgSuccess(`成功添加${newProducts.length}个商品`);
  361. emit('update:visible', false);
  362. };
  363. /** 查询分类树 */
  364. const getCategoryTree = async () => {
  365. const res = await categoryTree();
  366. categoryOptions.value = res.data || [];
  367. };
  368. onMounted(() => {
  369. getList();
  370. getCategoryTree();
  371. });
  372. </script>
  373. <style scoped>
  374. /* 核心:禁止外层滚动,仅表格内部滚动 */
  375. .product-dialog {
  376. overflow: hidden !important;
  377. }
  378. .product-dialog :deep(.el-dialog__body) {
  379. padding: 0;
  380. display: flex;
  381. flex-direction: column;
  382. overflow: hidden !important; /* 禁止对话框主体滚动 */
  383. height: calc(90vh); /* 限制对话框主体高度,避免超出视口 */
  384. }
  385. .dialog-content {
  386. display: flex;
  387. flex-direction: column;
  388. width: 100%;
  389. height: 100%;
  390. overflow: hidden !important; /* 禁止内容容器滚动 */
  391. }
  392. .search-container {
  393. flex-shrink: 0;
  394. padding: 16px;
  395. background: #f5f7fa;
  396. border-bottom: 1px solid #e4e7ed;
  397. box-sizing: border-box; /* 内边距不影响宽度 */
  398. width: 100%;
  399. }
  400. .search-container :deep(.el-form) {
  401. margin-bottom: 0;
  402. width: 100%;
  403. }
  404. .search-container :deep(.el-row) {
  405. display: flex;
  406. align-items: center;
  407. width: 100%;
  408. }
  409. .search-container .first-row {
  410. margin-bottom: 16px;
  411. }
  412. .search-container :deep(.el-form-item) {
  413. margin-bottom: 0;
  414. }
  415. .search-container :deep(.el-form-item__label) {
  416. font-weight: normal;
  417. white-space: nowrap;
  418. }
  419. .search-container :deep(.el-input),
  420. .search-container :deep(.el-select) {
  421. width: 100%;
  422. }
  423. /* 表格容器:仅让表格内部滚动,外层不滚动 */
  424. .table-wrapper {
  425. flex: 1; /* 占满剩余高度 */
  426. padding: 16px;
  427. box-sizing: border-box;
  428. width: 100%;
  429. overflow: hidden !important; /* 禁止表格容器滚动 */
  430. }
  431. .pagination-container {
  432. margin-top: 16px;
  433. display: flex;
  434. justify-content: flex-end;
  435. width: 100%;
  436. flex-shrink: 0; /* 分页栏不压缩 */
  437. }
  438. /* 修复固定列样式 */
  439. .product-dialog :deep(.el-table__fixed-right) {
  440. height: calc(100% - 17px) !important; /* 匹配表格高度 */
  441. }
  442. </style>