itemList.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. <template>
  2. <div class="config-container">
  3. <!-- 搜索区域 -->
  4. <div class="search-section">
  5. <div class="search-inputs">
  6. <el-input
  7. v-model="params.order_name"
  8. placeholder="搜索配置项名称"
  9. prefix-icon="el-icon-search"
  10. clearable
  11. @clear="searchOption"
  12. @input="searchOption"
  13. />
  14. <el-input
  15. v-model="params.keyword"
  16. placeholder="搜索商品名称"
  17. prefix-icon="el-icon-search"
  18. clearable
  19. @clear="searchOption"
  20. @input="searchOption"
  21. />
  22. </div>
  23. <el-button type="primary" @click="addOptionValues">新增配置项</el-button>
  24. </div>
  25. <!-- 配置项列表 -->
  26. <el-table :data="optionList.data" style="margin-top: 20px;">
  27. <el-table-column prop="name" label="配置项名称" width="160"/>
  28. <el-table-column prop="type" label="类型" width="110">
  29. <template slot-scope="scope">
  30. <el-tag :type="getTypeTag(scope.row.type)">
  31. {{ getTypeLabel(scope.row.type) }}
  32. </el-tag>
  33. </template>
  34. </el-table-column>
  35. <el-table-column prop="values" label="可选内容">
  36. <template slot-scope="scope">
  37. <span v-if="scope.row.option_values && scope.row.option_values.length">
  38. <span v-for="(v, idx) in scope.row.option_values.filter(v => v.status !== 0)" :key="idx">
  39. <el-tag size="small" type="info">{{ v.value }}</el-tag>
  40. <el-tag v-if="v.material_code" size="small" type="warning" style="margin-left:2px;">({{ v.material_code }})</el-tag>
  41. <span v-if="idx !== scope.row.option_values.filter(v => v.status !== 0).length - 1"> / </span>
  42. </span>
  43. </span>
  44. <span v-else>—</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="适用商品" min-width="200">
  48. <template slot-scope="scope">
  49. <div v-if="scope.row.show_product_names && scope.row.show_product_names.length > 0">
  50. <el-tag
  51. v-for="(productName, index) in scope.row.show_product_names"
  52. :key="index"
  53. style="margin-right: 5px; margin-bottom: 5px; cursor: pointer;"
  54. @click="handleProductTagClick(productName)"
  55. closable
  56. @close="handleProductTagClose(scope.row, index)"
  57. v-show="index < 6 || scope.row.productsExpanded"
  58. >
  59. {{ productName }}
  60. </el-tag>
  61. </div>
  62. <div class="product-actions">
  63. <el-button
  64. v-if="scope.row.show_product_names && scope.row.show_product_names.length > 6"
  65. type="text"
  66. size="mini"
  67. @click="toggleProductsExpanded(scope.row)"
  68. style="padding: 0; margin-right: 10px;">
  69. {{ scope.row.productsExpanded ? '收起' : `查看更多(${scope.row.show_product_names.length - 6})` }}
  70. </el-button>
  71. <el-button v-if="goodId" type="text" size="mini" @click="setProductAPI(scope.row)">配置此商品</el-button>
  72. <el-button v-if="!goodId" type="text" size="mini" @click="showProductSelector(scope.row)">选择商品</el-button>
  73. </div>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="操作" width="180">
  77. <template slot-scope="scope">
  78. <el-button size="mini" @click="editOption(scope.row)">编辑</el-button>
  79. <el-button size="mini" type="danger" @click="deleteOption(scope.row)">删除</el-button>
  80. </template>
  81. </el-table-column>
  82. </el-table>
  83. <!-- 新增/编辑配置项弹窗 -->
  84. <el-dialog :title="editMode ? '编辑配置项' : '新增配置项'" :visible.sync="showAddDialog" width="50%">
  85. <el-form :model="optionForm" label-width="120px" class="option-form">
  86. <el-form-item label="配置项名称" prop="name">
  87. <el-input v-model="optionForm.name" style="width: 400px;"/>
  88. </el-form-item>
  89. <el-form-item label="类型" prop="type">
  90. <el-select v-model="optionForm.type" style="width: 400px;">
  91. <el-option label="单选" value="radio"/>
  92. <el-option label="多选" value="checkbox"/>
  93. <el-option label="文本" value="text"/>
  94. </el-select>
  95. </el-form-item>
  96. <el-form-item label="可选内容" v-if="optionForm.type === 'radio' || optionForm.type === 'checkbox'" prop="option_values">
  97. <div class="option-values">
  98. <div v-for="(item, idx) in optionForm.option_values" :key="idx" class="option-item">
  99. <el-input v-show="item.status"
  100. v-model="optionForm.option_values[idx].value"
  101. placeholder="输入选项"
  102. style="width: 180px; margin-right: 10px;"
  103. />
  104. <el-input v-show="item.status"
  105. v-model="optionForm.option_values[idx].material_code"
  106. placeholder="物料编号"
  107. style="width: 180px; margin-right: 10px;"
  108. />
  109. <el-button
  110. v-show="item.status"
  111. icon="el-icon-delete"
  112. @click="removeOption(item, idx)"
  113. type="danger"
  114. ></el-button>
  115. </div>
  116. <el-button size="mini" type="primary" @click="optionForm.option_values.push({value: '', material_code: '', status: 1})">
  117. <i class="el-icon-plus"></i> 添加选项
  118. </el-button>
  119. </div>
  120. </el-form-item>
  121. </el-form>
  122. <div slot="footer">
  123. <el-button @click="showAddDialog = false">取消</el-button>
  124. <el-button type="primary" @click="saveOption">保存</el-button>
  125. </div>
  126. </el-dialog>
  127. <!-- 选择商品弹窗 -->
  128. <el-dialog title="选择适用商品" :visible.sync="showProductSelectorDialog" width="70%">
  129. <div class="product-selector">
  130. <div class="search-section">
  131. <el-input
  132. v-model="productSearchQuery"
  133. placeholder="搜索商品名称"
  134. prefix-icon="el-icon-search"
  135. clearable
  136. @clear="handleProductSearch"
  137. @input="handleProductSearch"
  138. style="width: 300px; margin-right: 10px;"
  139. />
  140. </div>
  141. <div class="product-list">
  142. <el-table :data="filteredProducts.data" style="width: 100%" height="400" @selection-change="handleProductSelectionChange" v-loading="loading">
  143. <el-table-column type="selection" width="55"></el-table-column>
  144. <el-table-column prop="name" label="商品名称" min-width="200"></el-table-column>
  145. <el-table-column label="是否可用" prop="enabled" width="100">
  146. <template slot-scope="scope">
  147. <el-tag :type="scope.row.enabled ? 'success' : 'danger'" size="mini" effect="dark">
  148. {{ scope.row.enabled ? '可用' : '不可用' }}
  149. </el-tag>
  150. </template>
  151. </el-table-column>
  152. <el-table-column prop="no" label="生产型号" width="160"></el-table-column>
  153. <el-table-column prop="type" label="类型" width="120"></el-table-column>
  154. </el-table>
  155. <el-pagination
  156. v-if="filteredProducts"
  157. slot="pagination"
  158. @size-change="handleSizeChange"
  159. @current-change="handleCurrentChange"
  160. :current-page="filteredProducts.page_no"
  161. :page-sizes="[20, 50, 100, 200]"
  162. :page-size="filteredProducts.page_size"
  163. layout="total, sizes, prev, pager, next, jumper"
  164. :total="filteredProducts.data_total">
  165. </el-pagination>
  166. </div>
  167. </div>
  168. <div slot="footer">
  169. <el-button @click="showProductSelectorDialog = false">取消</el-button>
  170. <el-button type="primary" @click="saveProductSelection">确定</el-button>
  171. </div>
  172. </el-dialog>
  173. <el-pagination
  174. v-if="optionList"
  175. slot="pagination"
  176. @size-change="handlePageSizeChange"
  177. @current-change="handlePageCurrentChange"
  178. :current-page="optionList.page_no"
  179. :page-sizes="[20, 50, 100, 200]"
  180. :page-size="optionList.page_size"
  181. layout="total, sizes, prev, pager, next, jumper"
  182. :total="optionList.data_total">
  183. </el-pagination>
  184. </div>
  185. </template>
  186. <script>
  187. import * as API_goods from '@/api/goods'
  188. import * as API_configOptions from '@/api/pjConfigOptions'
  189. export default {
  190. name: 'pjGoodsList',
  191. props: {
  192. goodId: {
  193. type: Number,
  194. default: 0
  195. },
  196. goodName: {
  197. type: String,
  198. default: ''
  199. }
  200. },
  201. data() {
  202. return {
  203. products: [],
  204. optionList: [],
  205. searchQuery: '',
  206. showAddDialog: false,
  207. showProductSelectorDialog: false,
  208. editMode: false,
  209. optionForm: {
  210. id: null,
  211. name: '',
  212. type: 'radio',
  213. option_values: [{ value: '', material_code: '', status: 1 }],
  214. productIds: []
  215. },
  216. currentEditingOption: null,
  217. selectedProductIds: [],
  218. selectedProductNames: [],
  219. productSearchQuery: '',
  220. filteredProducts: [],
  221. loading: false,
  222. checkIds: [],
  223. checkNames: [],
  224. goodParams: {
  225. page_no: 1,
  226. page_size: 20,
  227. // fixedCondition: ' enabled = 1 ',
  228. sort: 'create_time',
  229. dir: 'desc'
  230. },
  231. configLoading: false,
  232. /** 列表参数 */
  233. params: {
  234. page_no: 1,
  235. page_size: 20,
  236. sort: 'create_time',
  237. dir: 'desc'
  238. }
  239. }
  240. },
  241. created() {
  242. this.loadOptions()
  243. },
  244. methods: {
  245. async loadOptions() {
  246. this.configLoading = true
  247. try {
  248. let res = await API_configOptions.getList(this.params)
  249. res.data.forEach(item => {
  250. if (item.product_names) {
  251. item.show_product_names = item.product_names.split('=')
  252. }
  253. if (item.product_ids) {
  254. item.show_product_ids = item.product_ids.split(',')
  255. }
  256. // 初始化商品展开状态
  257. item.productsExpanded = false
  258. })
  259. this.optionList = res
  260. } catch (error) {
  261. this.$message.error('加载配置项失败')
  262. } finally {
  263. this.configLoading = false
  264. }
  265. },
  266. /** 分页大小发生改变 */
  267. handlePageSizeChange(size) {
  268. this.params.page_size = size
  269. this.loadOptions()
  270. },
  271. /** 分页页数发生改变 */
  272. handlePageCurrentChange(page) {
  273. this.params.page_no = page
  274. this.loadOptions()
  275. },
  276. addOptionValues() {
  277. this.editMode = false
  278. this.optionForm = {
  279. id: null,
  280. name: '',
  281. type: 'radio',
  282. option_values: [{ value: '', material_code: '', status: 1 }]
  283. }
  284. this.showAddDialog = true
  285. },
  286. getTypeLabel(type) {
  287. const types = {
  288. radio: '单选',
  289. checkbox: '多选',
  290. text: '文本'
  291. }
  292. return types[type] || type
  293. },
  294. getTypeTag(type) {
  295. const types = {
  296. radio: 'primary',
  297. checkbox: 'success',
  298. text: 'info'
  299. }
  300. return types[type] || 'info'
  301. },
  302. searchOption() {
  303. this.params.page_no = 1
  304. this.loadOptions()
  305. },
  306. handleProductTagClick(name) {
  307. this.params.keyword = name
  308. this.searchOption()
  309. },
  310. async handleProductTagClose(row, index) {
  311. try {
  312. await this.$confirm('确定删除商品吗?')
  313. row.show_product_ids.splice(index, 1)
  314. row.show_product_names.splice(index, 1)
  315. const data = {
  316. id: row.id,
  317. product_ids: row.show_product_ids.join(','),
  318. product_names: row.show_product_names.join('=')
  319. }
  320. await API_configOptions.editModel(data)
  321. this.$message.success('删除成功')
  322. this.loadOptions() // 重新加载列表
  323. } catch (error) {
  324. if (error !== 'cancel') {
  325. this.$message.error('删除失败')
  326. }
  327. }
  328. },
  329. showProductSelector(option) {
  330. this.currentEditingOption = option
  331. if (option.show_product_ids) {
  332. this.checkIds = [...option.show_product_ids]
  333. this.checkNames = [...option.show_product_names]
  334. } else {
  335. this.checkIds = []
  336. this.checkNames = []
  337. }
  338. this.loadProducts()
  339. this.showProductSelectorDialog = true
  340. },
  341. async loadProducts() {
  342. this.loading = true
  343. try {
  344. this.filteredProducts = await API_goods.getGoodsList(this.goodParams)
  345. } catch (error) {
  346. this.$message.error('加载商品列表失败')
  347. } finally {
  348. this.loading = false
  349. }
  350. },
  351. handleSizeChange(val) {
  352. this.goodParams.page_size = val
  353. this.loadProducts()
  354. },
  355. handleCurrentChange(val) {
  356. this.goodParams.page_no = val
  357. this.loadProducts()
  358. },
  359. handleProductSearch() {
  360. this.goodParams = {
  361. ...this.goodParams,
  362. query: this.productSearchQuery
  363. }
  364. this.loadProducts()
  365. },
  366. async setProductAPI(option) {
  367. if (!this.goodId) {
  368. this.$message.error('请先选择商品')
  369. return
  370. }
  371. if (option.show_product_ids) {
  372. this.checkIds = [...option.show_product_ids]
  373. this.checkNames = [...option.show_product_names]
  374. } else {
  375. this.checkIds = []
  376. this.checkNames = []
  377. }
  378. if (this.checkIds.indexOf(this.goodId + '') !== -1) {
  379. this.$message.error('商品已存在')
  380. return
  381. }
  382. this.checkIds.push(this.goodId + '')
  383. this.checkNames.push(this.goodName)
  384. await API_configOptions.editModel({
  385. id: option.id,
  386. product_ids: this.checkIds.join(','),
  387. product_names: this.checkNames.join('=')
  388. })
  389. this.$message.success('保存成功')
  390. this.loadOptions() // 重新加载列表
  391. },
  392. handleProductSelectionChange(selection) {
  393. this.selectedProductIds = selection.map(item => item.id)
  394. this.selectedProductNames = selection.map(item => item.name)
  395. },
  396. async saveProductSelection() {
  397. if (this.currentEditingOption) {
  398. try {
  399. // 过滤掉已存在的商品ID,避免重复添加
  400. this.checkIds.map(id => this.selectedProductIds.push(Number(id)))
  401. this.checkNames.map(name => this.selectedProductNames.push(name))
  402. // 数组去重
  403. this.selectedProductIds = [...new Set(this.selectedProductIds)]
  404. this.selectedProductNames = [...new Set(this.selectedProductNames)]
  405. const productNames = this.selectedProductNames.join('=')
  406. const productIds = this.selectedProductIds.join(',')
  407. await API_configOptions.editModel({
  408. id: this.currentEditingOption.id,
  409. product_ids: productIds,
  410. product_names: productNames
  411. })
  412. this.currentEditingOption.productIds = [...this.selectedProductIds]
  413. this.$message.success('保存成功')
  414. this.loadOptions() // 重新加载列表
  415. } catch (error) {
  416. this.$message.error('保存失败')
  417. }
  418. }
  419. this.showProductSelectorDialog = false
  420. },
  421. editOption(row) {
  422. this.editMode = true
  423. this.optionForm = JSON.parse(JSON.stringify(row))
  424. this.showAddDialog = true
  425. },
  426. async deleteOption(row) {
  427. try {
  428. await this.$confirm('确定删除该配置项吗?')
  429. await API_configOptions.deletes(row.id)
  430. this.$message.success('删除成功')
  431. this.loadOptions() // 重新加载列表
  432. } catch (error) {
  433. if (error !== 'cancel') {
  434. this.$message.error('删除失败')
  435. }
  436. }
  437. },
  438. removeOption(item, index) {
  439. if (this.optionForm.option_values.length === 1 || this.optionForm.option_values.filter(v => v.status === 1).length === 1) {
  440. this.$message.warning('至少保留一个选项')
  441. return
  442. }
  443. this.$confirm('确定要删除这个选项吗?', '提示', {
  444. confirmButtonText: '确定',
  445. cancelButtonText: '取消',
  446. type: 'warning'
  447. }).then(() => {
  448. if (this.editMode && item.id) {
  449. this.optionForm.option_values[index].status = 0
  450. // API_configOptions.deletesOptionValues(item.id)
  451. } else {
  452. this.optionForm.option_values.splice(index, 1)
  453. }
  454. this.$message.success('删除成功')
  455. }).catch(() => {})
  456. },
  457. async saveOption() {
  458. if (!this.optionForm.name) {
  459. this.$message.warning('请输入配置项名称')
  460. return
  461. }
  462. if ((this.optionForm.type === 'radio' || this.optionForm.type === 'checkbox') && !this.optionForm.option_values) {
  463. this.$message.warning('请填写可选内容')
  464. return
  465. }
  466. // 新增校验:每个选项的 value 和 material_code 都必填
  467. if ((this.optionForm.type === 'radio' || this.optionForm.type === 'checkbox')) {
  468. const empty = this.optionForm.option_values.some(v => v.status && (!v.value || !v.material_code))
  469. if (empty) {
  470. this.$message.warning('请填写所有选项的内容和物料编号')
  471. return
  472. }
  473. }
  474. try {
  475. if (this.editMode) {
  476. await API_configOptions.update(this.optionForm)
  477. } else {
  478. // const data = {
  479. // name: this.optionForm.name,
  480. // type: this.optionForm.type,
  481. // option_values: this.optionForm.option_values
  482. // .filter(v => v.status)
  483. // .map(v => ({ value: v.value, material_code: v.material_code }))
  484. // }
  485. await API_configOptions.addModel(this.optionForm)
  486. }
  487. this.$message.success('保存成功')
  488. this.showAddDialog = false
  489. this.editMode = false
  490. this.optionForm = { id: null, name: '', type: 'radio', option_values: [{ value: '', material_code: '', status: 1 }], productIds: [] }
  491. this.loadOptions() // 重新加载列表
  492. } catch (error) {
  493. this.$message.error('保存失败')
  494. }
  495. },
  496. toggleProductsExpanded(item) {
  497. this.$set(item, 'productsExpanded', !item.productsExpanded)
  498. }
  499. }
  500. }
  501. </script>
  502. <style scoped>
  503. .config-container {
  504. padding: 20px;
  505. }
  506. .search-section {
  507. display: flex;
  508. justify-content: space-between;
  509. align-items: center;
  510. margin-bottom: 20px;
  511. }
  512. .search-inputs {
  513. display: flex;
  514. gap: 10px;
  515. flex: 1;
  516. margin-right: 20px;
  517. }
  518. .search-inputs .el-input {
  519. width: 300px;
  520. }
  521. .search-inputs .el-select {
  522. width: 200px;
  523. }
  524. .option-form {
  525. padding: 20px;
  526. }
  527. .option-values {
  528. display: flex;
  529. flex-direction: column;
  530. gap: 10px;
  531. }
  532. .option-item {
  533. display: flex;
  534. align-items: center;
  535. margin-bottom: 10px;
  536. }
  537. .product-selector {
  538. .search-section {
  539. margin-bottom: 20px;
  540. display: flex;
  541. align-items: center;
  542. }
  543. .product-list {
  544. margin-top: 20px;
  545. }
  546. }
  547. .el-tag {
  548. cursor: pointer;
  549. transition: all 0.3s;
  550. }
  551. .el-tag:hover {
  552. opacity: 0.8;
  553. }
  554. .product-actions {
  555. display: flex;
  556. align-items: center;
  557. }
  558. </style>