accountList.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <div class="container">
  3. <Breadcrumb />
  4. <a-card title="定制电费提醒">
  5. <a-button type="primary" size="large" @click="editAccount()">
  6. <template #icon>
  7. <icon-plus />
  8. </template>
  9. 添加提醒任务
  10. </a-button>
  11. <a-alert v-if="notice" style="margin-top: 15px;">{{ notice }}</a-alert>
  12. <a-table :data="data" :columns="columns" :bordered="false" hoverable class="table" :loading="loading" :scroll="{
  13. x: 1600
  14. }" :pagination="{ showPageSize: true, showJumper: true, defaultPageSize: 15 }">
  15. <template #name-filter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset }">
  16. <div class="custom-filter">
  17. <a-space direction="vertical">
  18. <a-input :model-value="filterValue[0]" @input="(value) => setFilterValue([value])" />
  19. <div class="custom-filter-footer">
  20. <a-button @click="handleFilterReset">重置</a-button>
  21. <a-button @click="handleFilterConfirm">确定</a-button>
  22. </div>
  23. </a-space>
  24. </div>
  25. </template>
  26. <template #balance="{ record }">
  27. <a-tag :color="record.balance <= record.lowest ? 'red' : 'green'">¥{{ record.balance
  28. }}</a-tag>
  29. </template>
  30. <template #lowest="{ record }">
  31. ¥{{ record.lowest }}
  32. </template>
  33. <template #update_time="{ record }">
  34. {{ stramptoTime(record.update_time) }}
  35. </template>
  36. <template #optional="{ record }">
  37. <div style="display: flex; gap:10px">
  38. <a-button @click="GetChangeRecord(record.id)">账单&nbsp;<icon-select-all /></a-button>
  39. <a-dropdown :popup-max-height="false" trigger="hover">
  40. <a-button>操作&nbsp;<icon-down /></a-button>
  41. <template #content>
  42. <a-doption @click="editAccount(record)"><icon-edit /> 编辑任务</a-doption>
  43. <a-doption @click="DeleteAccount(record)"><icon-delete /> 删除任务</a-doption>
  44. </template>
  45. </a-dropdown>
  46. </div>
  47. </template>
  48. </a-table>
  49. </a-card>
  50. </div>
  51. <!-- 账号编辑对话框 -->
  52. <a-modal v-model:visible="visible" title="编辑电费提醒任务" @cancel="handleCancel" @before-ok="handleBeforeOk" draggable
  53. :ok-loading="ok_loading" esc-to-close closable>
  54. <a-form :model="form">
  55. <a-form-item field="area" label="校区" :loading="selectLoading">
  56. <a-select v-model="form.area" placeholder="请选择所在校区" default-value="" :options="areas"
  57. @change="GetPowerData('dylist', form.area)" />
  58. </a-form-item>
  59. <a-form-item field="building" label="楼栋" :loading="selectLoading">
  60. <a-select v-model="form.building" placeholder="请选择所在楼栋" default-value="" :options="buildings"
  61. @change="GetPowerData('mphlist', form.building)" />
  62. </a-form-item>
  63. <a-form-item field="room" label="宿舍号" :loading="selectLoading">
  64. <a-select v-model="form.room" placeholder="请选择所在宿舍" default-value="" :options="rooms" />
  65. </a-form-item>
  66. <a-form-item field="lowest" label="提醒阈值">
  67. <a-input-number v-model="form.lowest" placeholder="请选择提醒阈值" :step="1" :precision="2" />
  68. </a-form-item>
  69. <a-form-item field="email" label="通知邮箱">
  70. <EmailAutoComplete v-model="form.email" placeholder="用于接收电费变更通知" allow-clear />
  71. </a-form-item>
  72. <a-form-item field="notes" label="备注">
  73. <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
  74. show-word-limit />
  75. </a-form-item>
  76. </a-form>
  77. </a-modal>
  78. <!-- 电费变更记录 -->
  79. <a-modal v-model:visible="listVisible" title="电费变更记录" esc-to-close closable width="auto" hide-cancel>
  80. <a-table :bordered="false" :data="changeList" :columns="listColumns" stripe hoverable :loading="listLoading" :pagination="{
  81. showPageSize: true,
  82. showJumper: true,
  83. showTotal: true,
  84. pageSize: pagination.pagesize,
  85. current: pagination.current,
  86. total: pagination.total
  87. }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
  88. <template #balance="{ record }">
  89. ¥{{ record.balance }}
  90. </template>
  91. <template #old_balance="{ record }">
  92. ¥{{ record.old_balance }}
  93. </template>
  94. <template #change="{ record }">
  95. <a-tag :color="Number(record.balance) < Number(record.old_balance) ? 'red' : 'green'">¥{{ Number(record.balance
  96. - record.old_balance).toFixed(2)
  97. }}</a-tag>
  98. </template>
  99. <template #time="{ record }">
  100. {{ stramptoTime(record.time) }}
  101. </template>
  102. </a-table>
  103. </a-modal>
  104. </template>
  105. <script setup>
  106. import { ref, reactive, onMounted, h } from 'vue'
  107. import { getPowerData, addAccount, deleteAccount, getAccount, getChangeRecord } from '@/api/power'
  108. import { Modal, Notification, Message } from '@arco-design/web-vue'
  109. import { useRoute } from 'vue-router'
  110. import { getNotice } from '@/utils/util'
  111. const notice = ref('')
  112. const GetNotice = async () => {
  113. const { path } = useRoute()
  114. const res = await getNotice(path)
  115. notice.value = res
  116. }
  117. const data = ref([])
  118. const loading = ref(false)
  119. const listVisible = ref(false)
  120. const pagination = reactive({
  121. total: 0,
  122. current: 1,
  123. pagesize: 15
  124. })
  125. // 分页 - 页码变化
  126. const handlePageChange = (page) => {
  127. pagination.current = page
  128. }
  129. // 分页 - 每页条数变化
  130. const handlePageSizeChange = (size) => {
  131. pagination.pagesize = size
  132. pagination.current = 1 // 页大小变化后回到第一页
  133. }
  134. const changeList = ref([])
  135. const listLoading = ref(false)
  136. const GetChangeRecord = async (id) => {
  137. try {
  138. listLoading.value = true
  139. pagination.current = 1
  140. listVisible.value = true
  141. changeList.value = []
  142. const res = await getChangeRecord({ id })
  143. if (!res || res.code !== 0)
  144. return Notification.error({
  145. title: '获取电费变更记录失败!',
  146. content: res?.msg ?? '请稍后再试'
  147. })
  148. changeList.value = res.data
  149. pagination.total = res.data.length
  150. } catch (error) {
  151. Notification.error({
  152. title: '获取电费变更记录失败!',
  153. content: error.message || '请稍后再试'
  154. })
  155. } finally {
  156. listLoading.value = false
  157. }
  158. }
  159. const visible = ref(false)
  160. const ok_loading = ref(false)
  161. const form = reactive({
  162. id: null,
  163. area: '',
  164. building: '',
  165. room: '',
  166. email: '',
  167. notes: '',
  168. lowest: 10.00
  169. })
  170. const columns = [{
  171. title: '校区',
  172. dataIndex: 'area',
  173. width: 120
  174. }, {
  175. title: '楼栋',
  176. dataIndex: 'building',
  177. width: 130
  178. }, {
  179. title: '寝室号',
  180. dataIndex: 'room',
  181. }, {
  182. title: '通知邮箱',
  183. dataIndex: 'email',
  184. width: 220
  185. }, {
  186. title: '当前余额',
  187. slotName: 'balance',
  188. width: 120
  189. }, {
  190. title: '触发提醒阈值',
  191. slotName: 'lowest',
  192. width: 120
  193. }, {
  194. title: '扣费日期',
  195. dataIndex: 'koufei_date',
  196. width: 170
  197. }, {
  198. title: '上次更新时间',
  199. slotName: 'update_time',
  200. width: 170
  201. }, {
  202. title: '备注',
  203. dataIndex: 'notes'
  204. }, {
  205. title: '操作',
  206. slotName: 'optional',
  207. width: 200,
  208. fixed: 'right'
  209. }]
  210. const listColumns = [{
  211. title: '记录时间',
  212. slotName: 'time',
  213. width: 170
  214. }, {
  215. title: '扣费时间',
  216. dataIndex: 'change_time',
  217. width: 170
  218. }, {
  219. title: '电费余额',
  220. slotName: 'balance',
  221. width: 120
  222. }, {
  223. title: '原余额',
  224. slotName: 'old_balance',
  225. width: 120
  226. }, {
  227. title: '变动金额',
  228. slotName: 'change',
  229. width: 120
  230. }]
  231. const selectLoading = ref(false)
  232. const areas = ref([])
  233. const buildings = ref([])
  234. const rooms = ref([])
  235. const GetPowerData = async (type, pid = '') => {
  236. try {
  237. const data = {
  238. type,
  239. pid
  240. }
  241. const res = await getPowerData(data)
  242. if (!res || res.code !== 0)
  243. return Notification.error({
  244. title: '获取电费信息失败!',
  245. content: res?.msg ?? '请稍后再试'
  246. })
  247. let resdata = res.data.map(item => ({
  248. value: item,
  249. label: item
  250. }))
  251. switch (type) {
  252. case 'buildlist':
  253. areas.value = resdata
  254. break
  255. case 'dylist':
  256. buildings.value = resdata
  257. form.building = ''
  258. form.room = ''
  259. break
  260. case 'mphlist':
  261. rooms.value = resdata
  262. form.room = ''
  263. break
  264. }
  265. } catch (error) {
  266. Notification.error({
  267. title: '获取电费信息失败!',
  268. content: error.message || '请稍后再试'
  269. })
  270. }
  271. }
  272. const editAccount = (item) => {
  273. if (item) {
  274. form.id = item.id
  275. form.area = item.area
  276. form.building = item.building
  277. form.room = item.room
  278. form.email = item.email
  279. form.notes = item.notes
  280. form.lowest = Number(item.lowest) ?? 10.00
  281. } else {
  282. form.id = null
  283. form.area = ''
  284. form.building = ''
  285. form.room = ''
  286. form.email = ''
  287. form.notes = ''
  288. form.lowest = 10.00
  289. }
  290. visible.value = true
  291. }
  292. const handleBeforeOk = async (done) => {
  293. try {
  294. ok_loading.value = true
  295. const { area, building, room, email } = form
  296. if (!area || !building || !room || !email) {
  297. Message.error('请填写完整的任务信息')
  298. return false
  299. }
  300. const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
  301. if (!emailRegex.test(email)) {
  302. Message.error('请检查邮箱格式是否正确')
  303. return false
  304. }
  305. let data = {
  306. ...form
  307. }
  308. const res = await addAccount(data)
  309. if (!res || res.code !== 0) {
  310. Notification.error({
  311. title: '保存电费提醒任务失败!',
  312. content: res?.msg ?? '请稍后再试'
  313. })
  314. return false
  315. }
  316. Message.success('保存成功!')
  317. done()
  318. getAccounts()
  319. } catch (error) {
  320. Notification.error({
  321. title: '保存电费提醒任务失败!',
  322. content: error.message || '请稍后再试'
  323. })
  324. return false
  325. } finally {
  326. ok_loading.value = false
  327. }
  328. }
  329. const handleCancel = () => {
  330. visible.value = false
  331. }
  332. const getAccounts = async () => {
  333. try {
  334. loading.value = true
  335. const res = await getAccount()
  336. if (!res || res.code !== 0)
  337. return Notification.error({
  338. title: '获取账号列表失败!',
  339. content: res?.msg ?? '请稍后再试'
  340. })
  341. data.value = res.data
  342. } catch (error) {
  343. Notification.error({
  344. title: '获取账号列表失败!',
  345. content: error.message || '请稍后再试'
  346. })
  347. } finally {
  348. loading.value = false
  349. }
  350. }
  351. const DeleteAccount = async (item) => {
  352. Modal.confirm({
  353. title: '删除任务',
  354. content: () => h('div', [
  355. h('p', '您是否要删除该电费提醒任务?该操作不可逆')
  356. ]),
  357. onOk: async () => {
  358. const res = await deleteAccount({ id: item.id })
  359. if (!res || res.code !== 0)
  360. return Notification.error({
  361. title: '删除失败',
  362. content: res?.msg ?? '请稍后再试'
  363. })
  364. Message.success('删除成功!')
  365. getAccounts()
  366. }
  367. })
  368. }
  369. const stramptoTime = (time) => {
  370. return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
  371. }
  372. onMounted(() => {
  373. GetPowerData('buildlist')
  374. getAccounts()
  375. GetNotice()
  376. })
  377. </script>
  378. <style scoped lang="less">
  379. .container {
  380. padding: 0 20px 20px 20px;
  381. }
  382. .table {
  383. margin-top: 15px;
  384. .state {
  385. display: flex;
  386. align-items: center;
  387. .circle {
  388. border-radius: 50%;
  389. height: 8px;
  390. min-height: 8px;
  391. width: 8px;
  392. min-width: 8px;
  393. margin-right: 5px;
  394. }
  395. .zero {
  396. background-color: rgb(var(--orange-6));
  397. }
  398. .one {
  399. background-color: rgb(var(--green-6));
  400. }
  401. .else {
  402. background-color: rgb(var(--red-6));
  403. }
  404. }
  405. }
  406. .custom-filter {
  407. padding: 20px;
  408. background: var(--color-bg-5);
  409. border: 1px solid var(--color-neutral-3);
  410. border-radius: var(--border-radius-medium);
  411. box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
  412. }
  413. .custom-filter-footer {
  414. display: flex;
  415. justify-content: space-between;
  416. }
  417. </style>