LimitPurchaseRule.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <template>
  2. <div class="limit-purchase-rule-container">
  3. <!-- 规则列表区域 -->
  4. <el-card class="rule-list-card">
  5. <div slot="header" class="card-header">
  6. <span>限购规则列表</span>
  7. <el-button type="primary" size="small" @click="handleCreateRule">
  8. 新建规则
  9. </el-button>
  10. </div>
  11. <el-table
  12. v-loading="loading.list"
  13. :data="ruleList"
  14. border
  15. style="width: 100%"
  16. >
  17. <el-table-column prop="id" label="ID" width="80" />
  18. <el-table-column prop="name" label="规则名称" min-width="160" />
  19. <el-table-column label="限购地区" min-width="260">
  20. <template slot-scope="scope">
  21. <div v-if="scope.row.regions && scope.row.regions.length">
  22. <el-tag
  23. v-for="(region, index) in scope.row.regions"
  24. :key="index"
  25. type="danger"
  26. size="mini"
  27. class="region-tag"
  28. >
  29. {{ formatRegionLabel(region) }}
  30. </el-tag>
  31. </div>
  32. <span v-else class="text-muted">未设置</span>
  33. </template>
  34. </el-table-column>
  35. <el-table-column prop="status" label="状态" width="100">
  36. <template slot-scope="scope">
  37. <el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
  38. {{ scope.row.status === 1 ? '启用' : '停用' }}
  39. </el-tag>
  40. </template>
  41. </el-table-column>
  42. <el-table-column label="操作" width="200" fixed="right">
  43. <template slot-scope="scope">
  44. <el-button type="text" size="small" @click="handleEditRule(scope.row)">
  45. 编辑
  46. </el-button>
  47. <el-button type="text" size="small" @click="handleEditRegions(scope.row)">
  48. 设置限购地区
  49. </el-button>
  50. <el-button type="text" size="small" class="danger-text" @click="handleDeleteRule(scope.row)">
  51. 删除
  52. </el-button>
  53. </template>
  54. </el-table-column>
  55. </el-table>
  56. </el-card>
  57. <!-- 规则表单弹窗:新建 / 编辑规则基本信息 -->
  58. <el-dialog
  59. :title="ruleForm.id ? '编辑限购规则' : '新建限购规则'"
  60. :visible.sync="ruleDialog.visible"
  61. width="720px"
  62. :close-on-click-modal="false"
  63. >
  64. <el-form
  65. ref="ruleFormRef"
  66. :model="ruleForm"
  67. :rules="ruleRules"
  68. label-width="90px"
  69. >
  70. <el-form-item label="规则名称" prop="name">
  71. <el-input v-model="ruleForm.name" maxlength="64" show-word-limit />
  72. </el-form-item>
  73. <el-form-item label="状态" prop="status">
  74. <el-switch
  75. v-model="ruleForm.status"
  76. :active-value="1"
  77. :inactive-value="0"
  78. />
  79. </el-form-item>
  80. <!-- 在新建 / 编辑规则里直接设置限购地区(本地省市区 JSON 级联) -->
  81. <el-form-item label="限购地区">
  82. <div class="region-selector-wrapper">
  83. <el-form label-width="60px" class="region-form inner-region-form">
  84. <el-row :gutter="20" type="flex" align="middle">
  85. <el-col :span="6">
  86. <el-form-item label="省份">
  87. <el-select
  88. v-model="selectedProvince"
  89. placeholder="请选择省份"
  90. filterable
  91. clearable
  92. @change="onProvinceChange"
  93. >
  94. <el-option
  95. v-for="p in provinces"
  96. :key="p.name"
  97. :label="p.name"
  98. :value="p.name"
  99. />
  100. </el-select>
  101. </el-form-item>
  102. </el-col>
  103. <el-col :span="6">
  104. <el-form-item label="城市">
  105. <el-select
  106. v-model="selectedCity"
  107. placeholder="请选择城市"
  108. :disabled="!selectedProvince"
  109. filterable
  110. clearable
  111. @change="onCityChange"
  112. >
  113. <el-option
  114. v-for="c in cities"
  115. :key="c.name"
  116. :label="c.name"
  117. :value="c.name"
  118. />
  119. </el-select>
  120. </el-form-item>
  121. </el-col>
  122. <el-col :span="6">
  123. <el-form-item label="区县">
  124. <el-select
  125. v-model="selectedArea"
  126. placeholder="请选择区县"
  127. :disabled="!selectedCity"
  128. filterable
  129. clearable
  130. >
  131. <el-option
  132. v-for="a in areas"
  133. :key="a"
  134. :label="a"
  135. :value="a"
  136. />
  137. </el-select>
  138. </el-form-item>
  139. </el-col>
  140. <el-col :span="6" class="text-right">
  141. <el-button type="primary" @click="addRegion">
  142. <i class="el-icon-plus" /> 添加限购地区
  143. </el-button>
  144. </el-col>
  145. </el-row>
  146. </el-form>
  147. <div class="limited-region-list">
  148. <div class="list-title">已设置的限购地区</div>
  149. <div v-if="currentRuleRegions.length === 0" class="empty-tip">
  150. 暂未添加限购地区
  151. </div>
  152. <div
  153. v-for="(area, index) in currentRuleRegions"
  154. :key="index"
  155. class="limited-area-item"
  156. >
  157. <span>{{ formatRegionLabel(area) }}</span>
  158. <el-button
  159. type="text"
  160. class="danger-text"
  161. icon="el-icon-delete"
  162. @click="removeRegion(index)"
  163. >
  164. 删除
  165. </el-button>
  166. </div>
  167. </div>
  168. </div>
  169. </el-form-item>
  170. </el-form>
  171. <span slot="footer" class="dialog-footer">
  172. <el-button @click="ruleDialog.visible = false">取 消</el-button>
  173. <el-button type="primary" :loading="loading.saveRule" @click="submitRuleForm">
  174. 确 认
  175. </el-button>
  176. </span>
  177. </el-dialog>
  178. <!-- 地区设置弹窗:管理单条规则下的多个限购地区 -->
  179. <el-dialog
  180. title="设置限购地区"
  181. :visible.sync="regionDialog.visible"
  182. width="720px"
  183. :close-on-click-modal="false"
  184. >
  185. <div class="region-selector-wrapper">
  186. <div class="region-selector-title">
  187. <i class="el-icon-warning-outline text-danger" />
  188. <span> 设置限购地区</span>
  189. </div>
  190. <el-form label-width="60px" class="region-form">
  191. <el-row :gutter="20" type="flex" align="middle">
  192. <el-col :span="6">
  193. <el-form-item label="省份">
  194. <el-select
  195. v-model="selectedProvince"
  196. placeholder="请选择省份"
  197. filterable
  198. clearable
  199. @change="onProvinceChange"
  200. >
  201. <el-option
  202. v-for="p in provinces"
  203. :key="p.name"
  204. :label="p.name"
  205. :value="p.name"
  206. />
  207. </el-select>
  208. </el-form-item>
  209. </el-col>
  210. <el-col :span="6">
  211. <el-form-item label="城市">
  212. <el-select
  213. v-model="selectedCity"
  214. placeholder="请选择城市"
  215. :disabled="!selectedProvince"
  216. filterable
  217. clearable
  218. @change="onCityChange"
  219. >
  220. <el-option
  221. v-for="c in cities"
  222. :key="c.name"
  223. :label="c.name"
  224. :value="c.name"
  225. />
  226. </el-select>
  227. </el-form-item>
  228. </el-col>
  229. <el-col :span="6">
  230. <el-form-item label="区县">
  231. <el-select
  232. v-model="selectedArea"
  233. placeholder="请选择区县"
  234. :disabled="!selectedCity"
  235. filterable
  236. clearable
  237. >
  238. <el-option
  239. v-for="a in areas"
  240. :key="a"
  241. :label="a"
  242. :value="a"
  243. />
  244. </el-select>
  245. </el-form-item>
  246. </el-col>
  247. <el-col :span="6" class="text-right">
  248. <el-button type="primary" @click="addRegion">
  249. <i class="el-icon-plus" /> 添加限购地区
  250. </el-button>
  251. </el-col>
  252. </el-row>
  253. </el-form>
  254. <div class="limited-region-list">
  255. <div class="list-title">已设置的限购地区</div>
  256. <div v-if="currentRuleRegions.length === 0" class="empty-tip">
  257. 暂未添加限购地区
  258. </div>
  259. <div
  260. v-for="(area, index) in currentRuleRegions"
  261. :key="index"
  262. class="limited-area-item"
  263. >
  264. <span>{{ formatRegionLabel(area) }}</span>
  265. <el-button
  266. type="text"
  267. class="danger-text"
  268. icon="el-icon-delete"
  269. @click="removeRegion(index)"
  270. >
  271. 删除
  272. </el-button>
  273. </div>
  274. </div>
  275. </div>
  276. <span slot="footer" class="dialog-footer">
  277. <el-button @click="regionDialog.visible = false">取 消</el-button>
  278. <el-button type="primary" :loading="loading.saveRegions" @click="submitRegions">
  279. 确 认 设置
  280. </el-button>
  281. </span>
  282. </el-dialog>
  283. </div>
  284. </template>
  285. <script>
  286. // 前端内置省市区 JSON,不走接口
  287. // 这里只演示北京和天津两组数据,后续可按同样格式扩展
  288. const REGION_TREE = [
  289. {
  290. name: '北京市',
  291. city: [
  292. {
  293. name: '北京市',
  294. area: [
  295. '东城区',
  296. '西城区',
  297. '崇文区',
  298. '宣武区',
  299. '朝阳区',
  300. '丰台区',
  301. '石景山区',
  302. '海淀区',
  303. '门头沟区',
  304. '房山区',
  305. '通州区',
  306. '顺义区',
  307. '昌平区',
  308. '大兴区',
  309. '平谷区',
  310. '怀柔区',
  311. '密云县',
  312. '延庆县'
  313. ]
  314. }
  315. ]
  316. },
  317. {
  318. name: '天津市',
  319. city: [
  320. {
  321. name: '天津市',
  322. area: [
  323. '和平区',
  324. '河东区',
  325. '河西区',
  326. '南开区',
  327. '河北区',
  328. '红桥区',
  329. '塘沽区',
  330. '汉沽区',
  331. '大港区',
  332. '东丽区',
  333. '西青区',
  334. '津南区',
  335. '北辰区',
  336. '武清区',
  337. '宝坻区',
  338. '宁河县',
  339. '静海县',
  340. '蓟县'
  341. ]
  342. }
  343. ]
  344. }
  345. ]
  346. export default {
  347. name: 'LimitPurchaseRule',
  348. props: {
  349. axiosInstance: {
  350. type: Object,
  351. default: null
  352. }
  353. },
  354. data() {
  355. return {
  356. // 列表数据
  357. ruleList: [],
  358. loading: {
  359. list: false,
  360. saveRule: false,
  361. saveRegions: false
  362. },
  363. // 规则表单
  364. ruleDialog: {
  365. visible: false
  366. },
  367. ruleForm: {
  368. id: null,
  369. name: '',
  370. status: 1
  371. },
  372. ruleRules: {
  373. name: [
  374. { required: true, message: '请输入规则名称', trigger: 'blur' }
  375. ]
  376. },
  377. // 地区相关
  378. regionDialog: {
  379. visible: false,
  380. ruleId: null
  381. },
  382. // 使用本地省市区 JSON 级联
  383. provinces: REGION_TREE,
  384. cities: [],
  385. areas: [],
  386. selectedProvince: '',
  387. selectedCity: '',
  388. selectedArea: '',
  389. currentRuleRegions: [] // 当前规则下的地区数组
  390. }
  391. },
  392. created() {
  393. this.fetchRuleList()
  394. },
  395. methods: {
  396. http() {
  397. // 默认用全局 axios,如果父组件传入则优先用父组件的
  398. return this.axiosInstance || this.$axios || this.$http
  399. },
  400. // ========== 列表相关 ==========
  401. async fetchRuleList() {
  402. if (!this.http()) return
  403. this.loading.list = true
  404. try {
  405. const { data } = await this.http().get('/limit_purchase/rule')
  406. // 按项目通用返回结构处理
  407. this.ruleList = Array.isArray(data?.data) ? data.data : (data || [])
  408. } catch (e) {
  409. console.error(e)
  410. this.$message.error('加载规则列表失败')
  411. } finally {
  412. this.loading.list = false
  413. }
  414. },
  415. handleCreateRule() {
  416. this.ruleForm = {
  417. id: null,
  418. name: '',
  419. status: 1
  420. }
  421. // 新建规则时清空当前规则地区
  422. this.currentRuleRegions = []
  423. this.selectedProvince = ''
  424. this.selectedCity = ''
  425. this.selectedArea = ''
  426. this.cities = []
  427. this.areas = []
  428. this.ruleDialog.visible = true
  429. this.$nextTick(() => {
  430. this.$refs.ruleFormRef && this.$refs.ruleFormRef.clearValidate()
  431. })
  432. },
  433. handleEditRule(row) {
  434. this.ruleForm = {
  435. id: row.id,
  436. name: row.name,
  437. status: row.status
  438. }
  439. // 编辑规则时加载该规则已有的地区
  440. this.currentRuleRegions = Array.isArray(row.regions) ? [...row.regions] : []
  441. this.selectedProvince = ''
  442. this.selectedCity = ''
  443. this.selectedArea = ''
  444. this.cities = []
  445. this.areas = []
  446. this.ruleDialog.visible = true
  447. this.$nextTick(() => {
  448. this.$refs.ruleFormRef && this.$refs.ruleFormRef.clearValidate()
  449. })
  450. },
  451. submitRuleForm() {
  452. this.$refs.ruleFormRef.validate(async valid => {
  453. if (!valid || !this.http()) return
  454. this.loading.saveRule = true
  455. try {
  456. if (this.ruleForm.id) {
  457. // 更新:名称 + 状态 + 限购地区
  458. await this.http().put(`/limit_purchase/rule/${this.ruleForm.id}`, {
  459. name: this.ruleForm.name,
  460. status: this.ruleForm.status,
  461. regions: this.currentRuleRegions
  462. })
  463. this.$message.success('更新成功')
  464. } else {
  465. // 创建:名称 + 状态 + 限购地区
  466. await this.http().post('/limit_purchase/rule', {
  467. name: this.ruleForm.name,
  468. status: this.ruleForm.status,
  469. regions: this.currentRuleRegions
  470. })
  471. this.$message.success('创建成功')
  472. }
  473. this.ruleDialog.visible = false
  474. this.fetchRuleList()
  475. } catch (e) {
  476. console.error(e)
  477. this.$message.error('保存失败')
  478. } finally {
  479. this.loading.saveRule = false
  480. }
  481. })
  482. },
  483. handleDeleteRule(row) {
  484. if (!this.http()) return
  485. this.$confirm(`确认删除规则「${row.name}」吗?`, '提示', {
  486. type: 'warning'
  487. })
  488. .then(async () => {
  489. try {
  490. await this.http().delete(`/limit_purchase/rule/${row.id}`)
  491. this.$message.success('删除成功')
  492. this.fetchRuleList()
  493. } catch (e) {
  494. console.error(e)
  495. this.$message.error('删除失败')
  496. }
  497. })
  498. .catch(() => {})
  499. },
  500. // ========== 地区设置相关 ==========
  501. handleEditRegions(row) {
  502. if (!row || !row.id) return
  503. this.regionDialog.ruleId = row.id
  504. this.selectedProvince = ''
  505. this.selectedCity = ''
  506. this.selectedArea = ''
  507. this.cities = []
  508. this.areas = []
  509. this.currentRuleRegions = Array.isArray(row.regions) ? [...row.regions] : []
  510. this.regionDialog.visible = true
  511. },
  512. onProvinceChange() {
  513. this.selectedCity = ''
  514. this.selectedArea = ''
  515. this.areas = []
  516. const province = REGION_TREE.find(p => p.name === this.selectedProvince)
  517. this.cities = province ? province.city : []
  518. },
  519. onCityChange() {
  520. this.selectedArea = ''
  521. const province = REGION_TREE.find(p => p.name === this.selectedProvince)
  522. const city = province ? province.city.find(c => c.name === this.selectedCity) : null
  523. this.areas = city ? city.area : []
  524. },
  525. addRegion() {
  526. if (!this.selectedProvince || !this.selectedCity || !this.selectedArea) {
  527. this.$message.warning('请选择省、市、区县')
  528. return
  529. }
  530. const region = {
  531. province_id: null,
  532. province_name: this.selectedProvince,
  533. city_id: null,
  534. city_name: this.selectedCity,
  535. district_id: null,
  536. district_name: this.selectedArea
  537. }
  538. // 去重:相同省市区不重复添加
  539. const exists = this.currentRuleRegions.some(item =>
  540. item.province_id === region.province_id &&
  541. item.city_id === region.city_id &&
  542. item.district_id === region.district_id
  543. )
  544. if (exists) {
  545. this.$message.warning('该地区已在列表中')
  546. return
  547. }
  548. this.currentRuleRegions.push(region)
  549. },
  550. removeRegion(index) {
  551. this.currentRuleRegions.splice(index, 1)
  552. },
  553. async submitRegions() {
  554. if (!this.http() || !this.regionDialog.ruleId) return
  555. this.loading.saveRegions = true
  556. try {
  557. await this.http().post(`/limit_purchase/rule/${this.regionDialog.ruleId}/regions`, {
  558. regions: this.currentRuleRegions
  559. })
  560. this.$message.success('限购地区设置成功')
  561. this.regionDialog.visible = false
  562. this.fetchRuleList()
  563. } catch (e) {
  564. console.error(e)
  565. this.$message.error('保存限购地区失败')
  566. } finally {
  567. this.loading.saveRegions = false
  568. }
  569. },
  570. // ========== 工具方法 ==========
  571. formatRegionLabel(region) {
  572. const parts = []
  573. if (region.province_name) parts.push(region.province_name)
  574. if (region.city_name) parts.push(region.city_name)
  575. if (region.district_name) parts.push(region.district_name)
  576. return parts.join(' - ') || '未知地区'
  577. }
  578. }
  579. }
  580. </script>
  581. <style scoped>
  582. .limit-purchase-rule-container {
  583. padding: 0;
  584. }
  585. .rule-list-card {
  586. margin-bottom: 0;
  587. }
  588. .card-header {
  589. display: flex;
  590. justify-content: space-between;
  591. align-items: center;
  592. }
  593. .region-tag {
  594. margin-right: 4px;
  595. margin-bottom: 4px;
  596. }
  597. .text-muted {
  598. color: #999;
  599. }
  600. .danger-text {
  601. color: #f56c6c;
  602. }
  603. .region-selector-wrapper {
  604. padding: 10px 0 0;
  605. }
  606. .region-selector-title {
  607. display: flex;
  608. align-items: center;
  609. margin-bottom: 16px;
  610. font-size: 14px;
  611. }
  612. .region-selector-title .text-danger {
  613. color: #f56c6c;
  614. }
  615. .region-form {
  616. margin-bottom: 16px;
  617. }
  618. .limited-region-list {
  619. background: #fff7f7;
  620. border-radius: 4px;
  621. padding: 12px 16px;
  622. }
  623. .inner-region-form {
  624. margin-bottom: 8px;
  625. }
  626. .limited-region-list .list-title {
  627. font-size: 14px;
  628. margin-bottom: 8px;
  629. }
  630. .limited-region-list .empty-tip {
  631. font-size: 13px;
  632. color: #999;
  633. }
  634. .limited-area-item {
  635. display: flex;
  636. justify-content: space-between;
  637. align-items: center;
  638. padding: 8px 0;
  639. border-bottom: 1px solid #f5d1d1;
  640. }
  641. .limited-area-item:last-child {
  642. border-bottom: none;
  643. }
  644. .dialog-footer {
  645. text-align: right;
  646. }
  647. .text-right {
  648. text-align: right;
  649. }
  650. </style>