dialog.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <style scoped lang="scss">
  2. .device-order-container {
  3. padding: 10px 0;
  4. }
  5. .device-order-list {
  6. max-height: 400px;
  7. overflow-y: auto;
  8. }
  9. .device-order-item {
  10. display: flex;
  11. align-items: center;
  12. padding: 12px 16px;
  13. margin-bottom: 8px;
  14. background: #fafafa;
  15. border: 1px solid #ebeef5;
  16. border-radius: 6px;
  17. cursor: grab;
  18. transition: all 0.2s;
  19. user-select: none;
  20. &:hover {
  21. background: #f0f2f5;
  22. border-color: #d9dce1;
  23. }
  24. &.dragging {
  25. opacity: 0.5;
  26. background: #e6f7ff;
  27. border-color: #409eff;
  28. }
  29. &.drag-over {
  30. border-color: #409eff;
  31. background: #ecf5ff;
  32. }
  33. }
  34. .drag-handle {
  35. margin-right: 12px;
  36. cursor: grab;
  37. color: #c0c4cc;
  38. font-size: 18px;
  39. display: flex;
  40. align-items: center;
  41. &:active {
  42. cursor: grabbing;
  43. }
  44. }
  45. .order-sequence {
  46. display: flex;
  47. align-items: center;
  48. justify-content: center;
  49. width: 32px;
  50. height: 32px;
  51. background: #409eff;
  52. color: #fff;
  53. border-radius: 50%;
  54. font-size: 14px;
  55. font-weight: 600;
  56. margin-right: 16px;
  57. flex-shrink: 0;
  58. }
  59. .order-device-info {
  60. flex: 1;
  61. min-width: 0;
  62. .order-device-name {
  63. font-size: 14px;
  64. font-weight: 500;
  65. color: #303133;
  66. }
  67. .order-device-id {
  68. font-size: 12px;
  69. color: #909399;
  70. margin-top: 2px;
  71. }
  72. }
  73. .device-order-footer {
  74. margin-top: 20px;
  75. padding-top: 16px;
  76. border-top: 1px solid #ebeef5;
  77. text-align: right;
  78. }
  79. </style>
  80. <template>
  81. <div class="system-dialog-container">
  82. <el-dialog
  83. :title="state.dialog.title"
  84. v-model="state.dialog.isShowDialog"
  85. width="720px"
  86. draggable
  87. destroy-on-close
  88. :close-on-click-modal="false"
  89. align-center>
  90. <el-tabs v-model="state.tab" class="demo-tabs" style="width: 100%;" @tab-change="handleTabChange">
  91. <el-tab-pane label="基本信息" name="basic">
  92. <el-form
  93. inline
  94. :model="state.ruleForm"
  95. :rules="state.rules"
  96. label-position="top"
  97. ref="formRef"
  98. size="default"
  99. label-width="100px"
  100. class="mt5">
  101. <el-form-item label="站点ID:" prop="stationId" class="wd350">
  102. <el-input
  103. v-model="state.ruleForm.stationId"
  104. placeholder="站点ID"
  105. clearable
  106. class="wd100">
  107. </el-input>
  108. </el-form-item>
  109. <el-form-item label="工位数量:" prop="parkingNum">
  110. <el-input
  111. v-model="state.ruleForm.parkingNum"
  112. placeholder="工位数量"
  113. clearable
  114. class="wd100">
  115. </el-input>
  116. </el-form-item>
  117. <el-form-item label="站点名称:" prop="stationName">
  118. <el-input
  119. v-model="state.ruleForm.stationName"
  120. placeholder="站点名称"
  121. clearable
  122. class="wd200">
  123. </el-input>
  124. </el-form-item>
  125. <el-form-item label="站点照片:" prop="pictures">
  126. <ext-upload v-model="state.ruleForm.pictures" multiple max="6"></ext-upload>
  127. <!-- <el-input
  128. v-model="state.ruleForm.pictures"
  129. placeholder="站点照片"
  130. clearable
  131. class="wd350">
  132. </el-input>-->
  133. </el-form-item>
  134. <el-form-item class="w100"></el-form-item>
  135. <el-form-item label="站点状态:" prop="stationStatus">
  136. <ext-d-select type="WashStation.status" class="wd200" v-model="state.ruleForm.stationStatus"></ext-d-select>
  137. </el-form-item>
  138. <el-form-item label="站点类型:" prop="stationType">
  139. <ext-d-select type="WashStation.type" class="wd200" v-model="state.ruleForm.stationType"></ext-d-select>
  140. </el-form-item>
  141. <el-form-item label="运营主体:" prop="operationEntity" class="wd350">
  142. <el-input
  143. v-model="state.ruleForm.operationEntity"
  144. placeholder="运营主体"
  145. clearable
  146. class="wd100">
  147. </el-input>
  148. </el-form-item>
  149. <el-form-item label="统一社会信用代码:" prop="creditCode" class="wd350">
  150. <el-input
  151. v-model="state.ruleForm.creditCode"
  152. placeholder="统一社会信用代码"
  153. clearable
  154. class="wd100">
  155. </el-input>
  156. </el-form-item>
  157. <el-form-item label="场站联系人:" prop="contactPerson" class="wd350">
  158. <el-input
  159. v-model="state.ruleForm.contactPerson"
  160. placeholder="场站联系人"
  161. clearable
  162. class="wd100">
  163. </el-input>
  164. </el-form-item>
  165. <el-form-item label="地址:" prop="address" class="w100">
  166. <el-input
  167. v-model="state.ruleForm.address"
  168. placeholder="地址"
  169. clearable
  170. class="w100">
  171. </el-input>
  172. </el-form-item>
  173. <el-form-item label="经度坐标:" prop="location">
  174. <el-input
  175. v-model="state.location.stationLng"
  176. placeholder="经度坐标"
  177. clearable
  178. type="number"
  179. class="wd200">
  180. </el-input>
  181. </el-form-item>
  182. <el-form-item label="纬度坐标:" prop="location">
  183. <el-input
  184. v-model="state.location.stationLat"
  185. placeholder="纬度坐标"
  186. clearable
  187. type="number"
  188. class="wd200">
  189. </el-input>
  190. </el-form-item>
  191. <el-form-item label="服务电话:" prop="serviceTel">
  192. <el-input
  193. v-model="state.ruleForm.serviceTel"
  194. placeholder="服务电话"
  195. clearable
  196. class="wd350">
  197. </el-input>
  198. </el-form-item>
  199. <el-form-item label="站点电话:" prop="stationTel">
  200. <el-input
  201. v-model="state.ruleForm.stationTel"
  202. placeholder="站点电话"
  203. clearable
  204. class="wd350">
  205. </el-input>
  206. </el-form-item>
  207. <el-form-item label="营业时间描述:" prop="businessHours" class="w100">
  208. <el-input
  209. v-model="state.ruleForm.businessHours"
  210. placeholder="营业时间描述"
  211. clearable
  212. type="textarea"
  213. class="w100">
  214. </el-input>
  215. </el-form-item>
  216. <el-form-item label="停车费描述:" prop="parkingFee" class="w100">
  217. <el-input
  218. v-model="state.ruleForm.parkingFee"
  219. type="textarea"
  220. placeholder="停车费描述:eg:洗车费用满10元减免1小时停车费"
  221. clearable
  222. class="w100">
  223. </el-input>
  224. </el-form-item>
  225. <el-form-item label="停车费减免二维码文本:" prop="parkingQrCode" class="w100">
  226. <el-input
  227. v-model="state.ruleForm.parkingQrCode"
  228. type="textarea"
  229. placeholder="停车费减免二维码文本"
  230. clearable
  231. class="w100">
  232. </el-input>
  233. </el-form-item>
  234. <el-form-item label="备注:" prop="remark" class="w100">
  235. <el-input
  236. v-model="state.ruleForm.remark"
  237. placeholder="备注"
  238. clearable
  239. type="textarea"
  240. class="w100">
  241. </el-input>
  242. </el-form-item>
  243. </el-form>
  244. </el-tab-pane>
  245. <el-tab-pane label="费率信息" name="fee">
  246. <el-form
  247. inline
  248. :model="state.feeForm"
  249. :rules="state.feeRules"
  250. label-position="top"
  251. ref="feeFormRef"
  252. size="default"
  253. label-width="100px"
  254. class="mt5">
  255. <el-form-item label="站点" prop="feeRate" class="wd250">
  256. <ext-select
  257. v-model="state.feeForm.stationId"
  258. placeholder="站点"
  259. url="washStation/list"
  260. url-method="post"
  261. label-key="stationName"
  262. value-key="stationId"
  263. data-key="list"
  264. clearable
  265. disabled
  266. class="wd200 ml10"/>
  267. </el-form-item>
  268. <el-form-item label="绑定平台费率" prop="feeRate" class="wd250">
  269. <ext-select
  270. v-model="state.feeForm.feeRateId"
  271. placeholder="平台费率配置"
  272. clearable
  273. :dataList="state.platformFeeRateList"
  274. @on-change="handlePlatformFeeRateChange"
  275. class="wd200 ml10"/>
  276. </el-form-item>
  277. <el-form-item label="平台费率(0.1代表10%)" prop="feeRate" class="wd250">
  278. <el-input-number
  279. v-model="state.feeForm.feeRate"
  280. placeholder="平台费率(0.1代表10%)"
  281. clearable
  282. step="0.1"
  283. class="w100">
  284. </el-input-number>
  285. </el-form-item>
  286. <el-form-item label="充值冻结金额比例(0.3代表30%)" prop="frozenRatio" class="wd250">
  287. <el-input-number
  288. v-model="state.feeForm.frozenRatio"
  289. placeholder="充值冻结金额比例(0.3代表30%)"
  290. clearable
  291. step="0.1"
  292. class="w100">
  293. </el-input-number>
  294. </el-form-item>
  295. <el-form-item label="提现手续费率(0.006代表6‰)" prop="withdrawalFeeRate" class="wd250">
  296. <el-input-number
  297. v-model="state.feeForm.withdrawalFeeRate"
  298. placeholder="提现手续费率(0.006代表6‰)"
  299. clearable
  300. step="0.001"
  301. class="w100">
  302. </el-input-number>
  303. </el-form-item>
  304. </el-form>
  305. </el-tab-pane>
  306. <el-tab-pane label="调整设备顺序" name="deviceOrder" v-if="state.action !== 'add'">
  307. <div class="device-order-container" v-loading="state.deviceOrderLoading">
  308. <el-alert
  309. title="上下拖动设备行可调整工位顺序,调整后请点击下方保存按钮"
  310. type="info" :closable="false" show-icon style="margin-bottom: 16px;" />
  311. <div class="device-order-list" v-if="state.deviceOrderList.length > 0">
  312. <div
  313. v-for="(device, index) in state.deviceOrderList"
  314. :key="device.id"
  315. class="device-order-item"
  316. :class="{ 'dragging': state.dragIndex === index, 'drag-over': state.dragOverIndex === index }"
  317. draggable="true"
  318. @dragstart="handleDragStart($event, index)"
  319. @dragover.prevent="handleDragOver($event, index)"
  320. @drop="handleDrop($event, index)"
  321. @dragend="handleDragEnd"
  322. >
  323. <div class="drag-handle">
  324. <SvgIcon name="ele-Rank"/>
  325. </div>
  326. <div class="order-sequence">{{ index + 1 }}</div>
  327. <div class="order-device-info">
  328. <div class="order-device-name">{{ device.deviceName }}</div>
  329. <div class="order-device-id">{{ device.shortId || device.productKey }}</div>
  330. </div>
  331. </div>
  332. </div>
  333. <el-empty v-else description="该站点暂无设备" />
  334. <div class="device-order-footer">
  335. <el-button type="primary" @click="saveDeviceOrder" :loading="state.deviceOrderSaving" :disabled="state.deviceOrderList.length === 0">保存顺序</el-button>
  336. </div>
  337. </div>
  338. </el-tab-pane>
  339. </el-tabs>
  340. <!-- <el-form-item label="站点引导" prop="siteGuide">
  341. <el-input
  342. v-model="state.ruleForm.siteGuide"
  343. placeholder="站点引导"
  344. clearable
  345. class="wd350">
  346. </el-input>
  347. </el-form-item>-->
  348. <template #footer v-if="state.tab !== 'deviceOrder'">
  349. <span class="dialog-footer">
  350. <el-button @click="onCancel" size="default">取 消</el-button>
  351. <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">{{ state.dialog.submitTxt }}</el-button>
  352. </span>
  353. </template>
  354. </el-dialog>
  355. </div>
  356. </template>
  357. <script setup lang="ts" name="WashStationDialog">
  358. import {defineAsyncComponent, reactive, onMounted, ref} from 'vue';
  359. import {Msg} from "/@/utils/message";
  360. import {$body, $get} from "/@/utils/request";
  361. import u from '/@/utils/u'
  362. import ExtUpload from "/@/components/form/ExtUpload.vue";
  363. import ExtDSelect from "/@/components/form/ExtDSelect.vue";
  364. import ExtSelect from "/@/components/form/ExtSelect.vue";
  365. // 引入组件
  366. // 定义子组件向父组件传值/事件
  367. const emit = defineEmits(['refresh']);
  368. const formRef = ref();
  369. //定义初始变量,重置使用
  370. const initState = () => ({
  371. ruleForm: {
  372. id: null,
  373. location: {}
  374. },
  375. feeForm: {
  376. stationId: null as any
  377. },
  378. btnLoading: false,
  379. dialog: {
  380. isShowDialog: false,
  381. type: '',
  382. title: '',
  383. submitTxt: '',
  384. },
  385. rules: {},
  386. feeRules: {
  387. },
  388. location: {},
  389. tab: 'basic',
  390. platformFeeRateList: [],
  391. stationFeeRate: [],
  392. action:'',
  393. deviceOrderList: [] as any[],
  394. deviceOrderLoading: false,
  395. deviceOrderSaving: false,
  396. dragIndex: -1,
  397. dragOverIndex: -1,
  398. })
  399. // 定义变量内容
  400. const state = reactive(initState());
  401. // 打开弹窗
  402. const open = (action: string = 'add', row: any) => {
  403. state.action = action;
  404. state.dialog.title = u.dialog.actions[action].title + "『站点信息』"
  405. state.dialog.submitTxt = u.dialog.actions[action].btn + "『站点信息』"
  406. state.dialog.isShowDialog = true;
  407. if (action !== 'add') {
  408. loadData(row.id);
  409. state.feeForm.stationId = row.stationId
  410. loadStationFeeRate();
  411. loadDeviceListForStation(row.stationId);
  412. } else {
  413. state.ruleForm = Object.assign(state.ruleForm, row);
  414. }
  415. loadPlatformFeeRateList()
  416. };
  417. // 关闭弹窗
  418. const onClose = () => {
  419. state.dialog.isShowDialog = false;
  420. Object.assign(state, initState())
  421. };
  422. // 取消
  423. const onCancel = () => {
  424. onClose();
  425. };
  426. // 提交
  427. const onSubmit = () => {
  428. if(state.tab==='basic'){
  429. formRef.value.validate((v: boolean) => {
  430. if (v) {
  431. state.btnLoading = true;
  432. state.ruleForm.location = JSON.stringify(state.location)
  433. const url = !!state.ruleForm.id ? "washStation/modify" : "washStation/add"
  434. $body(url, state.ruleForm).then(() => {
  435. state.btnLoading = false;
  436. Msg.message('操作成功');
  437. //console.log('submit!')
  438. onClose();
  439. emit('refresh');
  440. }).catch(e=>{
  441. console.error(e);
  442. state.btnLoading = false;
  443. })
  444. } else {
  445. state.btnLoading = false;
  446. Msg.message('请先完整填写表单', 'error');
  447. }
  448. })
  449. }else{
  450. state.btnLoading = true;
  451. $body(`station-fee-rate/bind`,state.feeForm).then(res=>{
  452. Msg.message('站点费率绑定成功');
  453. state.btnLoading = false;
  454. onClose();
  455. emit('refresh');
  456. }).catch(e=>{
  457. Msg.message('操作失败','error');
  458. state.btnLoading = false;
  459. })
  460. }
  461. };
  462. const handleFormChange = (formData: any) => {
  463. //console.log(formData)
  464. }
  465. // 初始化数据
  466. const loadData = (id: number) => {
  467. $get(`washStation/detail/${id}`).then((res: any) => {
  468. state.ruleForm = res;
  469. state.location = JSON.parse(res.location)
  470. })
  471. }
  472. const loadStationFeeRate = () => {
  473. $get(`station-fee-rate/${state.feeForm.stationId}`).then(res => {
  474. state.feeForm = res;
  475. })
  476. }
  477. const loadPlatformFeeRateList = () => {
  478. $body(`platform-fee-rate/list`, {pageSize: 1024}).then(res => {
  479. state.platformFeeRateList = res.list;
  480. })
  481. }
  482. const loadDeviceListForStation = (stationId?: string) => {
  483. const sid = stationId || state.ruleForm.stationId
  484. if (!sid) return
  485. state.deviceOrderLoading = true
  486. $body('/washDevice/list', { stationId: sid, pageNum: 1, pageSize: 200 }).then((res: any) => {
  487. state.deviceOrderList = (res.list || []).map((d: any) => ({ ...d }))
  488. }).finally(() => {
  489. state.deviceOrderLoading = false
  490. })
  491. }
  492. const handleDragStart = (e: DragEvent, index: number) => {
  493. state.dragIndex = index
  494. if (e.dataTransfer) {
  495. e.dataTransfer.effectAllowed = 'move'
  496. }
  497. }
  498. const handleDragOver = (_e: DragEvent, index: number) => {
  499. state.dragOverIndex = index
  500. }
  501. const handleDrop = (_e: DragEvent, index: number) => {
  502. if (state.dragIndex === -1 || state.dragIndex === index) return
  503. const list = [...state.deviceOrderList]
  504. const [item] = list.splice(state.dragIndex, 1)
  505. list.splice(index, 0, item)
  506. state.deviceOrderList = list
  507. }
  508. const handleDragEnd = () => {
  509. state.dragIndex = -1
  510. state.dragOverIndex = -1
  511. }
  512. const saveDeviceOrder = () => {
  513. state.deviceOrderSaving = true
  514. const devices = state.deviceOrderList.map((d: any, idx: number) => ({
  515. id: d.id,
  516. sequence: idx + 1
  517. }))
  518. $body('/washDevice/batchUpdateSequence', devices).then(() => {
  519. Msg.message('设备顺序保存成功', 'success')
  520. loadDeviceListForStation()
  521. }).catch(() => {
  522. Msg.message('保存失败', 'error')
  523. }).finally(() => {
  524. state.deviceOrderSaving = false
  525. })
  526. }
  527. const handlePlatformFeeRateChange = (platformFeeRateId: any) => {
  528. console.log(platformFeeRateId)
  529. let rate = state.platformFeeRateList.find(k => k.id == platformFeeRateId);
  530. if (rate) {
  531. let {feeRate, withdrawalFeeRate, frozenRatio} = rate;
  532. state.feeForm = Object.assign({}, state.feeForm, {feeRate, withdrawalFeeRate, frozenRatio})
  533. } else {
  534. state.feeForm.withdrawalFeeRate = 0;
  535. state.feeForm.feeRate = 0;
  536. state.feeForm.frozenRatio = 0;
  537. }
  538. }
  539. const handleTabChange = (tab:string) => {
  540. if(tab==='basic'){
  541. state.dialog.submitTxt = u.dialog.actions[state.action].btn + "『站点信息』"
  542. }else if(tab==='fee'){
  543. state.dialog.submitTxt = u.dialog.actions[state.action].btn + "『站点费率』"
  544. }else if(tab==='deviceOrder'){
  545. loadDeviceListForStation()
  546. }
  547. }
  548. // 暴露变量
  549. defineExpose({
  550. open
  551. });
  552. </script>