From 4e59754839c5e78128730f97af2136c3f5e0e947 Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期三, 27 五月 2026 17:18:20 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue  |  134 ++++++
 admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue |  106 ++++
 admin/src/views/business/ywcustomerrechargerecord.vue                      |  169 +++++++
 admin/src/views/business/components/YwCustomerRechargeWindow.vue           |   48 ++
 admin/src/views/business/components/YwCustomerConditionerTab.vue           |  292 +++++++++++++
 admin/src/views/business/components/YwCustomerDeviceWindow.vue             |   97 ++++
 admin/src/views/business/components/YwCustomerElectricalTab.vue            |  175 +++++++
 admin/src/api/business/ywcustomerrecharge.js                               |   83 +++
 admin/src/views/business/ywcustomerrecharge.vue                            |  170 +++++++
 9 files changed, 1,274 insertions(+), 0 deletions(-)

diff --git a/admin/src/api/business/ywcustomerrecharge.js b/admin/src/api/business/ywcustomerrecharge.js
new file mode 100644
index 0000000..82a9a6f
--- /dev/null
+++ b/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 })
+}
diff --git a/admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue b/admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue
new file mode 100644
index 0000000..dc8fe75
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/components/YwCustomerConditionerTab.vue b/admin/src/views/business/components/YwCustomerConditionerTab.vue
new file mode 100644
index 0000000..e731bc2
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/components/YwCustomerDeviceWindow.vue b/admin/src/views/business/components/YwCustomerDeviceWindow.vue
new file mode 100644
index 0000000..d8e873a
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue b/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue
new file mode 100644
index 0000000..eabda81
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/components/YwCustomerElectricalTab.vue b/admin/src/views/business/components/YwCustomerElectricalTab.vue
new file mode 100644
index 0000000..4282abc
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/components/YwCustomerRechargeWindow.vue b/admin/src/views/business/components/YwCustomerRechargeWindow.vue
new file mode 100644
index 0000000..f2c99e4
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/ywcustomerrecharge.vue b/admin/src/views/business/ywcustomerrecharge.vue
new file mode 100644
index 0000000..c5c0e8a
--- /dev/null
+++ b/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>
diff --git a/admin/src/views/business/ywcustomerrechargerecord.vue b/admin/src/views/business/ywcustomerrechargerecord.vue
new file mode 100644
index 0000000..1c451d4
--- /dev/null
+++ b/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>

--
Gitblit v1.9.3