doum
9 天以前 4e59754839c5e78128730f97af2136c3f5e0e947
新增智能电表、空调管理
已添加9个文件
1274 ■■■■■ 文件已修改
admin/src/api/business/ywcustomerrecharge.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerConditionerTab.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerDeviceWindow.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerElectricalTab.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/components/YwCustomerRechargeWindow.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/ywcustomerrecharge.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/ywcustomerrechargerecord.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/business/ywcustomerrecharge.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
import request from '../../utils/request'
const base = '/visitsAdmin/cloudService/business/ywCustomerRecharge'
export function merchantPage (data) {
  return request.post(base + '/merchantPage', data, { trim: true })
}
export function getDetail (customerId) {
  return request.get(base + '/' + customerId + '/detail')
}
export function electricalPage (customerId, data) {
  return request.post(base + '/electrical/page?customerId=' + customerId, data, { trim: true })
}
export function selectableElectricalPage (customerId, data) {
  return request.post(base + '/electrical/selectablePage?customerId=' + customerId, data, { trim: true })
}
export function saveElectrical (data) {
  return request.post(base + '/electrical/save', data)
}
export function deleteElectrical (customerId, electricalId) {
  return request.get(base + '/electrical/delete?customerId=' + customerId + '&electricalId=' + electricalId)
}
export function conditionerPage (customerId, data) {
  return request.post(base + '/conditioner/page?customerId=' + customerId, data, { trim: true })
}
export function getGsConfig (customerId) {
  return request.get(base + '/conditioner/gsConfig?customerId=' + customerId)
}
export function saveGsConfig (data) {
  return request.post(base + '/conditioner/saveGsConfig', data)
}
export function rechargeElectrical (data) {
  return request.post(base + '/recharge/electrical', data)
}
export function resetElectrical (data) {
  return request.post(base + '/reset/electrical', data)
}
export function readMeter (customerId, electricalId) {
  return request.get(base + '/readMeter?customerId=' + customerId + '&electricalId=' + electricalId)
}
export function getElectricalRemoteInfo (electricalId) {
  return request.get(base + '/electrical/remoteInfo?electricalId=' + electricalId)
}
export function getConditionerRechargeInfo (customerId) {
  return request.get(base + '/recharge/conditioner/info?customerId=' + customerId)
}
export function rechargeConditioner (data) {
  return request.post(base + '/recharge/conditioner', data)
}
export function cleanConditioner (customerId) {
  return request.post(base + '/clean/conditioner?customerId=' + customerId)
}
export function rechargeRecordPage (data) {
  return request.post(base + '/rechargeRecord/page', data, { trim: true })
}
export function retryRecharge (id) {
  return request.post(base + '/rechargeRecord/retry/' + id)
}
export function syncRechargeStatus (id) {
  return request.post(base + '/rechargeRecord/sync/' + id)
}
export function exportRechargeRecord (data) {
  return request.post(base + '/rechargeRecord/exportExcel', data, { responseType: 'blob', trim: true })
}
admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
<template>
  <div v-loading="loading">
    <div class="info-block">
      <p>客户名称:{{ customer.name }}</p>
      <p>剩余金额(元):{{ leftMoney }}</p>
      <p>余额同步时间:{{ syncDate || '-' }}</p>
      <p v-if="platformInfo">总电量:{{ platformInfo.left_money_y != null ? platformInfo.left_money_y : '-' }}</p>
    </div>
    <el-form label-width="120px" size="small">
      <el-form-item label="充值金额">
        <el-input-number v-model="form.money" :min="0" :precision="2" style="width: 200px"/>
      </el-form-item>
      <el-form-item label="充值备注">
        <el-input v-model="form.remark" maxlength="300" style="width: 400px"/>
      </el-form-item>
    </el-form>
    <div class="footer-btns">
      <el-button type="primary" :loading="isOperating" v-permissions="['business:ywcustomerrecharge:recharge']" @click="confirmRecharge">确认充值</el-button>
      <el-button type="warning" plain :loading="isOperating" v-permissions="['business:ywcustomerrecharge:recharge']" @click="confirmClean">账户清零</el-button>
      <el-button @click="loadInfo">刷新余额</el-button>
    </div>
  </div>
</template>
<script>
import * as api from '@/api/business/ywcustomerrecharge'
export default {
  name: 'YwCustomerConditionerRechargePanel',
  props: {
    customer: { type: Object, default: () => ({}) }
  },
  data () {
    return {
      loading: false,
      isOperating: false,
      gsConfig: null,
      platformInfo: null,
      form: { money: 0, remark: '' }
    }
  },
  computed: {
    leftMoney () {
      if (this.gsConfig && this.gsConfig.leftMoney != null) return this.gsConfig.leftMoney
      return '-'
    },
    syncDate () {
      return this.gsConfig && this.gsConfig.syncDate
    }
  },
  mounted () {
    this.loadInfo()
  },
  methods: {
    loadInfo () {
      this.loading = true
      api.getConditionerRechargeInfo(this.customer.id)
        .then(res => {
          this.gsConfig = res.gsConfig || null
          this.platformInfo = res.platformInfo || null
        })
        .catch(e => this.$tip.apiFailed(e))
        .finally(() => { this.loading = false })
    },
    confirmRecharge () {
      this.$dialog.actionConfirm('确认充值吗?', '操作确认')
        .then(() => {
          this.isOperating = true
          return api.rechargeConditioner({
            customerId: this.customer.id,
            money: this.form.money,
            remark: this.form.remark
          })
        })
        .then(msg => {
          this.$tip.success(msg || '充值成功')
          this.form.money = 0
          this.loadInfo()
          this.$emit('success')
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
        .finally(() => { this.isOperating = false })
    },
    confirmClean () {
      this.$dialog.actionConfirm('确认清零空调账户吗?', '操作确认')
        .then(() => {
          this.isOperating = true
          return api.cleanConditioner(this.customer.id)
        })
        .then(msg => {
          this.$tip.success(msg || '清零成功')
          this.loadInfo()
          this.$emit('success')
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
        .finally(() => { this.isOperating = false })
    }
  }
}
</script>
<style scoped>
.info-block p { margin: 4px 0; line-height: 26px; }
.footer-btns { text-align: right; margin-top: 16px; }
.footer-btns .el-button { margin-left: 8px; }
</style>
admin/src/views/business/components/YwCustomerConditionerTab.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,292 @@
<template>
  <div v-loading="loading" class="conditioner-tab">
    <section class="config-section">
      <div class="section-title">计费配置</div>
      <el-form label-width="150px" size="small" class="config-form">
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="计费开关">
              <el-switch v-model="form.isPwr" :active-value="1" :inactive-value="0" active-text="开启" inactive-text="关闭"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="18:00-09:00 ä¸åœæœº">
              <el-switch v-model="form.isRestStop" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="欠费额度(元)">
              <el-input-number v-model="form.stopMoney" :min="0" :precision="2" :step="10" controls-position="right" style="width: 100%"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" class="remark-item">
          <el-input
            v-model="form.gsBz"
            type="textarea"
            :rows="2"
            maxlength="500"
            show-word-limit
            placeholder="请输入备注(选填)"
          />
        </el-form-item>
      </el-form>
    </section>
    <section class="config-section">
      <div class="section-header">
        <span class="section-title">关联内机</span>
        <el-button type="primary" size="small" icon="el-icon-plus" @click="openSelector">添加内机</el-button>
      </div>
      <el-table :data="form.conditioners" stripe size="small" class="device-table" empty-text="暂未关联内机,请点击添加">
        <el-table-column label="设备" min-width="200" align="left" show-overflow-tooltip>
          <template slot-scope="{ row }">{{ deviceLabel(row) }}</template>
        </el-table-column>
        <el-table-column prop="platformDevId" label="平台设备ID" min-width="110" align="center"/>
        <el-table-column label="在线" min-width="80" align="center">
          <template slot-scope="{ row }">
            <span :class="row.online === '在线' ? 'green' : 'red'">{{ row.online || '-' }}</span>
          </template>
        </el-table-column>
        <el-table-column label="电费占比%" min-width="130" align="center">
          <template slot-scope="{ row }">
            <el-input-number v-model="row.devRatio" :min="1" :max="100" size="small" controls-position="right"/>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80" align="center" fixed="right">
          <template slot-scope="{ $index }">
            <el-button type="text" class="red" @click="form.conditioners.splice($index, 1)">移除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </section>
    <div class="footer-btns">
      <el-button type="primary" :loading="saving" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="save">保存配置</el-button>
    </div>
    <GlobalWindow title="选择空调内机" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
      <el-form inline @submit.native.prevent class="selector-form">
        <el-form-item label="关键字">
          <el-input v-model="selectorKeyword" placeholder="名称/编号" clearable @keypress.enter.native="searchDevices"/>
        </el-form-item>
        <el-button type="primary" @click="searchDevices">查询</el-button>
      </el-form>
      <el-table ref="devTable" v-loading="selectorLoading" :data="selectorList" stripe size="small" @selection-change="rows => selectedRows = rows">
        <el-table-column type="selection" width="45"/>
        <el-table-column label="设备" min-width="180" align="left" show-overflow-tooltip>
          <template slot-scope="{ row }">{{ deviceLabel(row) }}</template>
        </el-table-column>
        <el-table-column prop="platformDevId" label="平台设备ID" min-width="110" align="center"/>
        <el-table-column prop="online" label="在线" min-width="80" align="center"/>
      </el-table>
      <pagination @current-change="p => { selectorPagination.pageIndex = p; loadDevices() }" :pagination="selectorPagination"/>
    </GlobalWindow>
  </div>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import Pagination from '@/components/common/Pagination'
import * as rechargeApi from '@/api/business/ywcustomerrecharge'
import * as conditionerApi from '@/api/business/ywconditioner'
export default {
  name: 'YwCustomerConditionerTab',
  components: { GlobalWindow, Pagination },
  props: {
    customerId: Number,
    active: Boolean
  },
  data () {
    return {
      loading: false,
      saving: false,
      form: {
        isPwr: 1,
        isRestStop: 0,
        stopMoney: 0,
        gsBz: '',
        conditioners: []
      },
      selectorVisible: false,
      selectorLoading: false,
      selectorKeyword: '',
      selectorList: [],
      selectorPagination: { pageIndex: 1, pageSize: 10, total: 0 },
      selectedRows: []
    }
  },
  watch: {
    active (val) {
      if (val) this.loadConfig()
    },
    customerId () {
      if (this.active) this.loadConfig()
    }
  },
  mounted () {
    if (this.active) this.loadConfig()
  },
  methods: {
    deviceLabel (row) {
      const parts = [row.floorName, row.roomName, row.name].filter(Boolean)
      return parts.length ? parts.join('/') : (row.name || row.code || '-')
    },
    loadConfig () {
      if (!this.customerId) return
      this.loading = true
      Promise.all([
        rechargeApi.getGsConfig(this.customerId),
        rechargeApi.conditionerPage(this.customerId, { page: 1, capacity: 500, model: {} })
      ]).then(([gs, page]) => {
        if (gs) {
          this.form.isPwr = gs.isPwr != null ? gs.isPwr : 1
          this.form.isRestStop = gs.isRestStop != null ? gs.isRestStop : 0
          this.form.stopMoney = gs.stopMoney != null ? gs.stopMoney : 0
          this.form.gsBz = gs.gsBz || ''
        } else {
          this.form.isPwr = 1
          this.form.isRestStop = 0
          this.form.stopMoney = 0
          this.form.gsBz = ''
        }
        this.form.conditioners = (page.records || []).map(c => ({
          conditionerId: c.id,
          platformDevId: c.platformDevId,
          name: c.name,
          floorName: c.floorName,
          roomName: c.roomName,
          online: c.online,
          devRatio: c.devRatio != null ? c.devRatio : 100
        }))
      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.loading = false })
    },
    save () {
      if (!this.form.conditioners.length) {
        this.$tip.warning('请至少关联一台内机')
        return
      }
      this.saving = true
      rechargeApi.saveGsConfig({
        customerId: this.customerId,
        isPwr: this.form.isPwr,
        isRestStop: this.form.isRestStop,
        stopMoney: this.form.stopMoney,
        gsBz: this.form.gsBz,
        conditioners: this.form.conditioners.map(c => ({
          conditionerId: c.conditionerId,
          devRatio: c.devRatio
        }))
      }).then(() => {
        this.$tip.success('保存成功')
        this.loadConfig()
        this.$emit('success')
      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.saving = false })
    },
    openSelector () {
      this.selectorVisible = true
      this.selectorKeyword = ''
      this.selectedRows = []
      this.selectorPagination.pageIndex = 1
      this.loadDevices()
    },
    loadDevices () {
      this.selectorLoading = true
      conditionerApi.fetchList({
        page: this.selectorPagination.pageIndex,
        capacity: this.selectorPagination.pageSize,
        model: this.selectorKeyword ? { devKeyword: this.selectorKeyword } : {}
      }).then(data => {
        const boundIds = new Set(this.form.conditioners.map(c => c.conditionerId))
        this.selectorList = (data.records || []).filter(r => !boundIds.has(r.id))
        this.selectorPagination.total = data.total || 0
      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.selectorLoading = false })
    },
    searchDevices () {
      this.selectorPagination.pageIndex = 1
      this.loadDevices()
    },
    confirmSelect () {
      if (!this.selectedRows.length) {
        this.$tip.warning('请选择内机')
        return
      }
      this.selectedRows.forEach(r => {
        this.form.conditioners.push({
          conditionerId: r.id,
          platformDevId: r.platformDevId,
          name: r.name,
          floorName: r.floorName,
          roomName: r.roomName,
          online: r.online,
          devRatio: 100
        })
      })
      this.selectorVisible = false
    }
  }
}
</script>
<style scoped>
.conditioner-tab {
  padding-top: 4px;
}
.config-section {
  margin-bottom: 16px;
  padding: 16px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 4px;
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.section-title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  line-height: 22px;
}
.section-header .section-title {
  margin-bottom: 0;
}
.config-section > .section-title {
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid #ebeef5;
}
.config-form {
  margin-bottom: 0;
}
.config-form ::v-deep .el-form-item {
  margin-bottom: 12px;
}
.config-form ::v-deep .remark-item {
  margin-bottom: 0;
}
.config-form ::v-deep .remark-item .el-textarea {
  max-width: 100%;
}
.config-form ::v-deep .el-switch {
  width: auto;
}
.device-table {
  width: 100%;
}
.footer-btns {
  text-align: right;
  padding-top: 4px;
}
.selector-form {
  margin-bottom: 12px;
}
.green { color: #67c23a; }
.red { color: #f56c6c; }
</style>
admin/src/views/business/components/YwCustomerDeviceWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
<template>
  <GlobalWindow title="关联设备" :visible.sync="visible" width="920px" :show-confirm="false">
    <div class="merchant-info">
      <div class="merchant-info__item">
        <span class="merchant-info__label">客户类型</span>
        <span class="merchant-info__value">{{ customerTypeText }}</span>
      </div>
      <div class="merchant-info__item">
        <span class="merchant-info__label">客户名称</span>
        <span class="merchant-info__value">{{ customer.name || '-' }}</span>
      </div>
      <div class="merchant-info__item">
        <span class="merchant-info__label">联系人</span>
        <span class="merchant-info__value">{{ customer.memberName || '-' }}</span>
      </div>
      <div class="merchant-info__item">
        <span class="merchant-info__label">联系方式</span>
        <span class="merchant-info__value">{{ customer.memberPhone || '-' }}</span>
      </div>
    </div>
    <el-tabs v-model="activeTab" class="device-tabs">
      <el-tab-pane label="关联电表" name="electrical">
        <YwCustomerElectricalTab :customer-id="customer.id" :active="activeTab === 'electrical'" @success="$emit('success')"/>
      </el-tab-pane>
      <el-tab-pane label="关联空调" name="conditioner">
        <YwCustomerConditionerTab :customer-id="customer.id" :active="activeTab === 'conditioner'" @success="$emit('success')"/>
      </el-tab-pane>
    </el-tabs>
  </GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import YwCustomerElectricalTab from './YwCustomerElectricalTab'
import YwCustomerConditionerTab from './YwCustomerConditionerTab'
export default {
  name: 'YwCustomerDeviceWindow',
  components: { GlobalWindow, YwCustomerElectricalTab, YwCustomerConditionerTab },
  data () {
    return {
      visible: false,
      activeTab: 'electrical',
      customer: {}
    }
  },
  computed: {
    customerTypeText () {
      const t = this.customer.type
      return t === 0 || t === '0' ? '个人' : '企业'
    }
  },
  methods: {
    open (row, tab) {
      this.customer = {
        id: row.id,
        type: row.type,
        name: row.name,
        memberName: row.memberName,
        memberPhone: row.memberPhone
      }
      this.activeTab = tab || 'electrical'
      this.visible = true
    }
  }
}
</script>
<style scoped>
.merchant-info {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 24px;
  margin-bottom: 16px;
  padding: 30px 16px 30px 16px;
  background: #f5f7fa;
  border-radius: 4px;
}
.merchant-info__item {
  min-width: 180px;
  line-height: 22px;
}
.merchant-info__label {
  color: #909399;
  margin-right: 8px;
}
.merchant-info__label::after {
  content: ':';
}
.merchant-info__value {
  color: #303133;
  font-weight: 500;
}
.device-tabs {
  margin-top: 4px;
}
</style>
admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<template>
  <div v-loading="loading">
    <el-form label-width="120px" size="small">
      <el-form-item label="选择电表">
        <el-select v-model="electricalId" placeholder="请选择电表" filterable style="width: 360px" @change="loadRemoteInfo">
          <el-option v-for="item in electricalList" :key="item.id" :label="item.name + ' (' + item.address + ')'" :value="item.id"/>
        </el-select>
      </el-form-item>
    </el-form>
    <account-recharge-panel
      v-if="electricalId"
      :info="info"
      :latest="latest"
      :form="form"
      :is-operating="isOperating"
      :purchase-count="purchaseCount"
      mode="recharge"
      @read="readMeter"
      @confirm="confirmRecharge"
    />
  <!--  <div v-if="electricalId" class="extra-btns">
      <el-button type="warning" plain :loading="isOperating" v-permissions="['business:ywcustomerrecharge:recharge']" @click="resetAccount('resetPrepay')">清零(预付费)</el-button>
      <el-button type="warning" plain :loading="isOperating" v-permissions="['business:ywcustomerrecharge:recharge']" @click="resetAccount('resetPostpay')">清零(后付费)</el-button>
    </div>
    -->
    <div v-if="!electricalList.length && !loading" class="empty-tip">该商户尚未关联电表,请先在关联设备中添加</div>
  </div>
</template>
<script>
import AccountRechargePanel from './AccountRechargePanel'
import * as api from '@/api/business/ywcustomerrecharge'
export default {
  name: 'YwCustomerElectricalRechargePanel',
  components: { AccountRechargePanel },
  props: {
    customer: { type: Object, default: () => ({}) }
  },
  data () {
    return {
      loading: false,
      electricalList: [],
      electricalId: null,
      info: {},
      latest: null,
      purchaseCount: '0',
      form: { money: 0, remark: '' },
      isOperating: false
    }
  },
  mounted () {
    this.loadElectricalList()
  },
  methods: {
    loadElectricalList () {
      this.loading = true
      api.electricalPage(this.customer.id, { page: 1, capacity: 500, model: {} })
        .then(data => {
          this.electricalList = data.records || []
          if (this.electricalList.length) {
            this.electricalId = this.electricalList[0].id
            this.loadRemoteInfo()
          }
        })
        .catch(e => this.$tip.apiFailed(e))
        .finally(() => { this.loading = false })
    },
    loadRemoteInfo () {
      if (!this.electricalId) return
      api.getElectricalRemoteInfo(this.electricalId).then(res => {
        this.info = res.electrical || {}
        this.latest = res.latestData
        this.purchaseCount = res.purchaseCount || '0'
      }).catch(e => this.$tip.apiFailed(e))
    },
    readMeter () {
      if (!this.electricalId) return
      this.isOperating = true
      api.readMeter(this.customer.id, this.electricalId)
        .then(res => {
          this.info = res.electrical || this.info
          this.latest = res.latestData
          this.$tip.success(res.message || '抄表完成')
        })
        .catch(e => this.$tip.apiFailed(e))
        .finally(() => { this.isOperating = false })
    },
    confirmRecharge () {
      this.$dialog.actionConfirm('确认充值吗?', '操作确认')
        .then(() => {
          this.isOperating = true
          return api.rechargeElectrical({
            customerId: this.customer.id,
            electricalId: this.electricalId,
            money: this.form.money,
            remark: this.form.remark
          })
        })
        .then(msg => {
          this.$tip.success(msg || '提交成功,请在充值记录中查看结果')
          this.loadRemoteInfo()
          this.$emit('success')
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
        .finally(() => { this.isOperating = false })
    },
    resetAccount (resetAction) {
      const label = resetAction === 'resetPrepay' ? '预付费' : '后付费'
      this.$dialog.actionConfirm('确认清零并切换到' + label + '模式吗?', '操作确认')
        .then(() => {
          this.isOperating = true
          return api.resetElectrical({
            customerId: this.customer.id,
            electricalId: this.electricalId,
            resetAction
          })
        })
        .then(msg => {
          this.$tip.success(msg || '提交成功')
          this.loadRemoteInfo()
          this.$emit('success')
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
        .finally(() => { this.isOperating = false })
    }
  }
}
</script>
<style scoped>
.extra-btns { margin-top: 12px; }
.empty-tip { padding: 24px; color: #909399; text-align: center; }
</style>
admin/src/views/business/components/YwCustomerElectricalTab.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
<template>
  <div>
    <div class="toolbar-row">
      <el-button type="primary" size="small" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="openSelector">去选择电表</el-button>
    </div>
    <el-table v-loading="loading" :data="list" stripe size="small">
      <el-table-column prop="name" label="名称" min-width="120" align="center" show-overflow-tooltip/>
      <el-table-column prop="address" label="表地址" min-width="130" align="center" show-overflow-tooltip/>
      <el-table-column prop="accountId" label="开户号" min-width="100" align="center"/>
      <el-table-column prop="roomNames" label="房间" min-width="120" align="center" show-overflow-tooltip/>
      <el-table-column label="在线" min-width="80" align="center">
        <template slot-scope="{ row }">
          <span :class="row.online === 1 ? 'green' : 'red'">{{ row.online === 1 ? '在线' : '离线' }}</span>
        </template>
      </el-table-column>
      <el-table-column label="继电器" min-width="80" align="center">
        <template slot-scope="{ row }">{{ relayText(row.relayStatus) }}</template>
      </el-table-column>
      <el-table-column label="操作" min-width="80" align="center">
        <template slot-scope="{ row }">
          <el-button type="text" class="red" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="remove(row)">移除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination small @size-change="onSizeChange" @current-change="onPageChange" :pagination="pagination"/>
    <GlobalWindow title="选择电表" :visible.sync="selectorVisible" width="780px" @confirm="confirmSelect">
      <el-form inline @submit.native.prevent>
        <el-form-item label="关键字">
          <el-input v-model="selectorKeyword" placeholder="名称/地址" clearable @keypress.enter.native="searchSelectable"/>
        </el-form-item>
        <el-button type="primary" @click="searchSelectable">查询</el-button>
      </el-form>
      <el-table ref="selectTable" v-loading="selectorLoading" :data="selectorList" stripe size="small" @selection-change="onSelectionChange">
        <el-table-column type="selection" width="45"/>
        <el-table-column prop="name" label="名称" min-width="120" align="center"/>
        <el-table-column prop="address" label="表地址" min-width="130" align="center"/>
        <el-table-column prop="roomNames" label="房间" min-width="120" align="center" show-overflow-tooltip/>
        <el-table-column label="在线" min-width="80" align="center">
          <template slot-scope="{ row }">
            <span :class="row.online === 1 ? 'green' : 'red'">{{ row.online === 1 ? '在线' : '离线' }}</span>
          </template>
        </el-table-column>
      </el-table>
      <pagination small @current-change="onSelectorPageChange" :pagination="selectorPagination"/>
    </GlobalWindow>
  </div>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import Pagination from '@/components/common/Pagination'
import * as api from '@/api/business/ywcustomerrecharge'
export default {
  name: 'YwCustomerElectricalTab',
  components: { GlobalWindow, Pagination },
  props: {
    customerId: Number,
    active: Boolean
  },
  data () {
    return {
      loading: false,
      list: [],
      pagination: { pageIndex: 1, pageSize: 10, total: 0 },
      selectorVisible: false,
      selectorLoading: false,
      selectorList: [],
      selectorKeyword: '',
      selectorPagination: { pageIndex: 1, pageSize: 10, total: 0 },
      selectedRows: []
    }
  },
  watch: {
    active (val) {
      if (val) this.loadList()
    },
    customerId () {
      if (this.active) this.loadList()
    }
  },
  mounted () {
    if (this.active) this.loadList()
  },
  methods: {
    relayText (v) {
      if (v === '0' || v === 0) return '拉闸'
      if (v === '1' || v === 1) return '合闸'
      return v || '-'
    },
    loadList () {
      if (!this.customerId) return
      this.loading = true
      api.electricalPage(this.customerId, {
        page: this.pagination.pageIndex,
        capacity: this.pagination.pageSize,
        model: {}
      }).then(data => {
        this.list = data.records || []
        this.pagination.total = data.total || 0
      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.loading = false })
    },
    onPageChange (p) {
      this.pagination.pageIndex = p
      this.loadList()
    },
    onSizeChange (s) {
      this.pagination.pageSize = s
      this.pagination.pageIndex = 1
      this.loadList()
    },
    remove (row) {
      this.$dialog.actionConfirm('确认移除该电表关联吗?', '操作确认')
        .then(() => api.deleteElectrical(this.customerId, row.id))
        .then(() => {
          this.$tip.success('已移除')
          this.loadList()
          this.$emit('success')
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
    },
    openSelector () {
      this.selectorVisible = true
      this.selectorKeyword = ''
      this.selectedRows = []
      this.selectorPagination.pageIndex = 1
      this.loadSelectable()
    },
    loadSelectable () {
      this.selectorLoading = true
      api.selectableElectricalPage(this.customerId, {
        page: this.selectorPagination.pageIndex,
        capacity: this.selectorPagination.pageSize,
        model: this.selectorKeyword ? { name: this.selectorKeyword } : {}
      }).then(data => {
        this.selectorList = data.records || []
        this.selectorPagination.total = data.total || 0
      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.selectorLoading = false })
    },
    searchSelectable () {
      this.selectorPagination.pageIndex = 1
      this.loadSelectable()
    },
    onSelectorPageChange (p) {
      this.selectorPagination.pageIndex = p
      this.loadSelectable()
    },
    onSelectionChange (rows) {
      this.selectedRows = rows
    },
    confirmSelect () {
      if (!this.selectedRows.length) {
        this.$tip.warning('请选择电表')
        return
      }
      api.saveElectrical({
        customerId: this.customerId,
        electricalIds: this.selectedRows.map(r => r.id)
      }).then(() => {
        this.$tip.success('保存成功')
        this.selectorVisible = false
        this.loadList()
        this.$emit('success')
      }).catch(e => this.$tip.apiFailed(e))
    }
  }
}
</script>
<style scoped>
.toolbar-row { margin-bottom: 12px; }
.green { color: #67c23a; }
.red { color: #f56c6c; }
</style>
admin/src/views/business/components/YwCustomerRechargeWindow.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
<template>
  <GlobalWindow :title="'充值 - ' + (customer.name || '')" :visible.sync="visible" width="820px" :show-confirm="false">
    <el-tabs v-model="activeTab">
      <el-tab-pane label="电表充值" name="electrical">
        <YwCustomerElectricalRechargePanel
          v-if="activeTab === 'electrical'"
          :customer="customer"
          @success="onSuccess"
        />
      </el-tab-pane>
      <el-tab-pane label="空调充值" name="conditioner">
        <YwCustomerConditionerRechargePanel
          v-if="activeTab === 'conditioner'"
          :customer="customer"
          @success="onSuccess"
        />
      </el-tab-pane>
    </el-tabs>
  </GlobalWindow>
</template>
<script>
import GlobalWindow from '@/components/common/GlobalWindow'
import YwCustomerElectricalRechargePanel from './YwCustomerElectricalRechargePanel'
import YwCustomerConditionerRechargePanel from './YwCustomerConditionerRechargePanel'
export default {
  name: 'YwCustomerRechargeWindow',
  components: { GlobalWindow, YwCustomerElectricalRechargePanel, YwCustomerConditionerRechargePanel },
  data () {
    return {
      visible: false,
      activeTab: 'electrical',
      customer: {}
    }
  },
  methods: {
    open (row, tab) {
      this.customer = { id: row.id, name: row.name }
      this.activeTab = tab || 'electrical'
      this.visible = true
    },
    onSuccess () {
      this.$emit('success')
    }
  }
}
</script>
admin/src/views/business/ywcustomerrecharge.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
<template>
  <TableLayout :permissions="['business:ywcustomerrecharge:query']">
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="客户名称" prop="nameKeyword">
        <el-input v-model="searchForm.nameKeyword" placeholder="客户名称" clearable @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="电表状态" prop="electricalStatusFilter">
        <el-select v-model="searchForm.electricalStatusFilter" clearable placeholder="全部" style="min-width: 120px">
          <el-option label="全在线" :value="1"/>
          <el-option label="存在离线" :value="2"/>
          <el-option label="无设备" :value="3"/>
        </el-select>
      </el-form-item>
      <el-form-item label="空调状态" prop="conditionerStatusFilter">
        <el-select v-model="searchForm.conditionerStatusFilter" clearable placeholder="全部" style="min-width: 120px">
          <el-option label="全在线" :value="1"/>
          <el-option label="存在离线" :value="2"/>
          <el-option label="无设备" :value="3"/>
        </el-select>
      </el-form-item>
      <section>
        <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
        <el-button icon="el-icon-refresh" @click="reset">重置</el-button>
      </section>
    </el-form>
    <template v-slot:table-wrap>
      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
        <el-table-column prop="type" label="客户类型" min-width="80" align="center">
          <template slot-scope="{ row }">
            <span>{{ row.type == 0 || row.type === '0' ? '个人' : '企业' }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="name" label="客户名称" min-width="140" align="center" show-overflow-tooltip/>
        <el-table-column prop="memberName" label="联系人" min-width="120" align="center" show-overflow-tooltip>
          <template slot-scope="{ row }">{{ row.memberName || '-' }}</template>
        </el-table-column>
        <el-table-column prop="memberPhone" label="联系电话" min-width="130" align="center">
          <template slot-scope="{ row }">{{ row.memberPhone || '-' }}</template>
        </el-table-column>
        <el-table-column prop="electricalCount" label="关联电表数" min-width="100" align="center"/>
        <el-table-column label="电表余额" min-width="140" align="center">
          <template slot-scope="{ row }">
            <span v-if="!row.electricalBalances || !row.electricalBalances.length" class="balance-text">-</span>
            <el-tooltip v-else placement="top" effect="dark">
              <div slot="content" class="balance-tooltip">
                <div v-for="(item, idx) in row.electricalBalances" :key="idx" class="balance-tooltip-line">
                  {{ item.name }}({{ item.address }}):{{ formatBalance(item.balance) }}
                </div>
              </div>
              <span class="balance-text balance-summary">
                <template v-for="(item, idx) in row.electricalBalances">
                  <span :key="'b-' + idx" :class="{ red: isAmountLow(item.balance) }">{{ formatBalance(item.balance) }}</span><span v-if="idx < row.electricalBalances.length - 1" :key="'s-' + idx">/</span>
                </template>
              </span>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="conditionerCount" label="空调内机数" min-width="100" align="center"/>
        <el-table-column label="空调余额" min-width="110" align="center">
          <template slot-scope="{ row }">
            <span v-if="row.acBalance == null" class="balance-text">-</span>
            <span v-else class="balance-text" :class="{ red: isAmountLow(row.acBalance) }">{{ row.acBalance }}</span>
          </template>
        </el-table-column>
        <el-table-column prop="createDate" label="创建时间" min-width="160" align="center"/>
        <el-table-column label="操作" min-width="180" align="center" fixed="right">
          <template slot-scope="{ row }">
            <el-button type="text" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="openDevice(row)">关联设备</el-button>
            <el-button type="text" v-permissions="['business:ywcustomerrecharge:recharge']" @click="openRecharge(row)">充值</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
    </template>
    <YwCustomerDeviceWindow ref="deviceWindow" @success="search"/>
    <YwCustomerRechargeWindow ref="rechargeWindow" @success="search"/>
  </TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
import * as api from '@/api/business/ywcustomerrecharge'
import YwCustomerDeviceWindow from './components/YwCustomerDeviceWindow'
import YwCustomerRechargeWindow from './components/YwCustomerRechargeWindow'
export default {
  name: 'YwCustomerRecharge',
  extends: BaseTable,
  components: { TableLayout, Pagination, YwCustomerDeviceWindow, YwCustomerRechargeWindow },
  data () {
    return {
      searchForm: {
        nameKeyword: '',
        electricalStatusFilter: null,
        conditionerStatusFilter: null
      }
    }
  },
  created () {
    this.search()
  },
  methods: {
    handlePageChange (pageIndex) {
      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
      this.loadList()
    },
    loadList () {
      this.isWorking.search = true
      api.merchantPage({
        page: this.tableData.pagination.pageIndex,
        capacity: this.tableData.pagination.pageSize,
        model: this.buildSearchModel()
      }).then(data => {
        this.tableData.list = data.records
        this.tableData.pagination.total = data.total
      }).catch(() => {}).finally(() => { this.isWorking.search = false })
    },
    search () {
      this.tableData.pagination.pageIndex = 1
      this.loadList()
    },
    buildSearchModel () {
      const model = {}
      if (this.searchForm.nameKeyword) model.nameKeyword = this.searchForm.nameKeyword
      if (this.searchForm.electricalStatusFilter != null) model.electricalStatusFilter = this.searchForm.electricalStatusFilter
      if (this.searchForm.conditionerStatusFilter != null) model.conditionerStatusFilter = this.searchForm.conditionerStatusFilter
      return model
    },
    reset () {
      this.searchForm = { nameKeyword: '', electricalStatusFilter: null, conditionerStatusFilter: null }
      this.search()
    },
    openDevice (row) {
      this.$refs.deviceWindow.open(row)
    },
    openRecharge (row) {
      this.$refs.rechargeWindow.open(row)
    },
    formatBalance (val) {
      if (val == null || val === '') return '0'
      const n = parseFloat(val)
      return isNaN(n) ? val : String(val)
    },
    isAmountLow (val) {
      if (val == null || val === '' || val === '-') return false
      const n = parseFloat(val)
      return !isNaN(n) && n <= 0
    }
  }
}
</script>
<style scoped>
.red { color: #f56c6c; }
.balance-text { white-space: nowrap; }
.balance-summary {
  cursor: default;
}
</style>
<style>
.balance-tooltip {
  line-height: 22px;
}
.balance-tooltip-line {
  white-space: nowrap;
}
</style>
admin/src/views/business/ywcustomerrechargerecord.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
<template>
  <TableLayout :permissions="['business:ywcustomerrechargerecord:query']">
    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
      <el-form-item label="客户名称" prop="customerName">
        <el-input v-model="searchForm.customerName" placeholder="客户名称" clearable @keypress.enter.native="search"/>
      </el-form-item>
      <el-form-item label="业务类型" prop="type">
        <el-select v-model="searchForm.type" clearable placeholder="全部" style="min-width: 120px">
          <el-option label="电表" :value="0"/>
          <el-option label="空调" :value="1"/>
        </el-select>
      </el-form-item>
      <el-form-item label="充值状态" prop="status">
        <el-select v-model="searchForm.status" clearable placeholder="全部" style="min-width: 120px">
          <el-option label="充值中" :value="0"/>
          <el-option label="充值成功" :value="1"/>
          <el-option label="充值失败" :value="2"/>
        </el-select>
      </el-form-item>
      <el-form-item label="提交时间">
        <el-date-picker
          v-model="searchForm.dateRange"
          type="datetimerange"
          value-format="yyyy-MM-dd HH:mm:ss"
          start-placeholder="开始"
          end-placeholder="结束"
          style="width: 360px"
        />
      </el-form-item>
      <section>
        <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
        <el-button icon="el-icon-refresh" @click="reset">重置</el-button>
      </section>
    </el-form>
    <template v-slot:table-wrap>
      <ul class="toolbar">
        <li>
          <el-button @click="exportExcel" :loading="isWorking.export" v-permissions="['business:ywcustomerrechargerecord:exportExcel']">导出</el-button>
        </li>
      </ul>
      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
        <el-table-column prop="customerName" label="客户名称" min-width="130" align="center" show-overflow-tooltip/>
        <el-table-column label="业务类型" min-width="90" align="center">
          <template slot-scope="{ row }">{{ row.type === 1 ? '空调' : '电表' }}</template>
        </el-table-column>
        <el-table-column prop="deviceInfo" label="设备信息" min-width="180" align="center" show-overflow-tooltip/>
        <el-table-column prop="money" label="充值金额(元)" min-width="110" align="center"/>
        <el-table-column prop="banlance" label="充值前余额" min-width="110" align="center"/>
        <el-table-column prop="balanceAfter" label="充值后余额" min-width="110" align="center"/>
        <el-table-column label="状态" min-width="100" align="center">
          <template slot-scope="{ row }">
            <span v-if="row.status === 0" class="orange">充值中</span>
            <span v-else-if="row.status === 1" class="green">充值成功</span>
            <span v-else-if="row.status === 2" class="red">充值失败</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="oprId" label="任务ID" min-width="180" align="center" show-overflow-tooltip/>
        <el-table-column prop="remark" label="备注" min-width="120" align="center" show-overflow-tooltip/>
        <el-table-column prop="statusInfo" label="状态说明" min-width="140" align="center" show-overflow-tooltip/>
        <el-table-column prop="createDate" label="提交时间" min-width="160" align="center"/>
        <el-table-column prop="statusTime" label="状态更新时间" min-width="160" align="center"/>
        <el-table-column label="操作" min-width="160" align="center" fixed="right">
          <template slot-scope="{ row }">
            <el-button v-if="row.status === 2" type="text" v-permissions="['business:ywcustomerrechargerecord:retry']" @click="handleRetry(row)">再次提交</el-button>
            <el-button v-if="row.status === 0" type="text" v-permissions="['business:ywcustomerrechargerecord:syncStatus']" @click="handleSync(row)">手动同步</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
    </template>
  </TableLayout>
</template>
<script>
import BaseTable from '@/components/base/BaseTable'
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
import * as api from '@/api/business/ywcustomerrecharge'
export default {
  name: 'YwCustomerRechargeRecord',
  extends: BaseTable,
  components: { TableLayout, Pagination },
  data () {
    return {
      searchForm: {
        customerName: '',
        type: null,
        status: null,
        dateRange: null
      }
    }
  },
  created () {
    this.search()
  },
  methods: {
    handlePageChange (pageIndex) {
      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
      this.loadList()
    },
    loadList () {
      this.isWorking.search = true
      api.rechargeRecordPage({
        page: this.tableData.pagination.pageIndex,
        capacity: this.tableData.pagination.pageSize,
        model: this.buildSearchModel()
      }).then(data => {
        this.tableData.list = data.records
        this.tableData.pagination.total = data.total
      }).catch(() => {}).finally(() => { this.isWorking.search = false })
    },
    search () {
      this.tableData.pagination.pageIndex = 1
      this.loadList()
    },
    buildSearchModel () {
      const model = {}
      if (this.searchForm.customerName) model.customerName = this.searchForm.customerName
      if (this.searchForm.type !== '' && this.searchForm.type !== null) model.type = this.searchForm.type
      if (this.searchForm.status !== '' && this.searchForm.status !== null) model.status = this.searchForm.status
      if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
        model.createTimeBegin = this.searchForm.dateRange[0]
        model.createTimeEnd = this.searchForm.dateRange[1]
      }
      return model
    },
    reset () {
      this.searchForm = { customerName: '', type: null, status: null, dateRange: null }
      this.search()
    },
    exportExcel () {
      this.$dialog.exportConfirm('确认导出吗?')
        .then(() => {
          this.isWorking.export = true
          api.exportRechargeRecord({ page: 1, capacity: 1000000, model: this.buildSearchModel() })
            .then(response => { this.download(response) })
            .catch(e => this.$tip.apiFailed(e))
            .finally(() => { this.isWorking.export = false })
        })
        .catch(() => {})
    },
    handleRetry (row) {
      this.$dialog.actionConfirm('确认再次提交该充值吗?', '操作确认')
        .then(() => api.retryRecharge(row.id))
        .then(msg => {
          this.$tip.success(msg || '已提交')
          this.loadList()
        })
        .catch(e => { if (e !== 'cancel') this.$tip.apiFailed(e) })
    },
    handleSync (row) {
      api.syncRechargeStatus(row.id)
        .then(msg => {
          this.$tip.success(msg || '同步完成')
          this.loadList()
        })
        .catch(e => this.$tip.apiFailed(e))
    }
  }
}
</script>
<style scoped>
.green { color: #67c23a; }
.red { color: #f56c6c; }
.orange { color: #e6a23c; }
</style>