Browse Source

feat: 美化宿舍电费提醒页面

采用云商城卡片布局,调整余额/阈值/扣费日期展示位置。

Co-authored-by: Cursor <cursoragent@cursor.com>
Pchen. 3 weeks ago
parent
commit
3c9047256c
1 changed files with 356 additions and 211 deletions
  1. 356 211
      src/pages/power/accountList.vue

+ 356 - 211
src/pages/power/accountList.vue

@@ -1,96 +1,161 @@
 <template>
-  <div class="container">
-    <Breadcrumb />
-
-    <a-card title="定制电费提醒">
-      <a-button type="primary" size="large" @click="editAccount()">
-        <template #icon>
-          <icon-plus />
-        </template>
-        添加提醒任务
-      </a-button>
-
-      <a-alert v-if="notice" style="margin-top: 15px;">{{ notice }}</a-alert>
-
-      <a-table :data="data" :columns="columns" :bordered="false" hoverable class="table" :loading="loading" :scroll="{
-        x: 1600
-      }" :pagination="{ showPageSize: true, showJumper: true, defaultPageSize: 15 }">
-
-        <template #name-filter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset }">
-          <div class="custom-filter">
-            <a-space direction="vertical">
-              <a-input :model-value="filterValue[0]" @input="(value) => setFilterValue([value])" />
-              <div class="custom-filter-footer">
-                <a-button @click="handleFilterReset">重置</a-button>
-                <a-button @click="handleFilterConfirm">确定</a-button>
+  <div class="store-page power-page">
+    <div class="power-page__inner">
+      <Breadcrumb />
+
+      <header class="page-header">
+        <div>
+          <h1 class="store-section-title">定制电费提醒</h1>
+          <p class="store-section-desc">绑定宿舍号,余额低于阈值时将通过邮件通知您</p>
+        </div>
+        <a-button type="primary" class="primary-btn" @click="editAccount()">
+          <template #icon><icon-plus /></template>
+          添加提醒任务
+        </a-button>
+      </header>
+
+      <a-alert v-if="notice" type="info" closable class="notice">{{ notice }}</a-alert>
+
+      <a-spin :loading="loading" class="store-spin">
+        <a-empty v-if="!loading && data.length === 0" description="暂无提醒任务">
+          <a-button type="primary" class="primary-btn" @click="editAccount()">添加第一个任务</a-button>
+        </a-empty>
+
+        <div v-else class="account-list">
+          <article
+            v-for="record in data"
+            :key="record.id"
+            class="account-card"
+            :class="{ 'account-card--low': isLowBalance(record) }"
+          >
+            <div class="account-card__head">
+              <div class="account-card__location">
+                <icon-home class="loc-icon" />
+                <span>{{ record.area }} · {{ record.building }} · {{ record.room }}</span>
               </div>
-            </a-space>
-          </div>
-        </template>
-        <template #balance="{ record }">
-          <a-tag :color="record.balance <= record.lowest ? 'red' : 'green'">¥{{ record.balance
-          }}</a-tag>
-        </template>
-        <template #lowest="{ record }">
-          ¥{{ record.lowest }}
-        </template>
-        <template #update_time="{ record }">
-          {{ stramptoTime(record.update_time) }}
-        </template>
-        <template #optional="{ record }">
-          <div style="display: flex; gap:10px">
-            <a-button @click="GetChangeRecord(record.id)">账单&nbsp;<icon-select-all /></a-button>
-            <a-dropdown :popup-max-height="false" trigger="hover">
-              <a-button>操作&nbsp;<icon-down /></a-button>
-              <template #content>
-                <a-doption @click="editAccount(record)"><icon-edit /> 编辑任务</a-doption>
-                <a-doption @click="DeleteAccount(record)"><icon-delete /> 删除任务</a-doption>
-              </template>
-            </a-dropdown>
-          </div>
-        </template>
-      </a-table>
-    </a-card>
+              <a-tag color="arcoblue" size="small">
+                扣费时间 {{ record.koufei_date || '-' }}
+              </a-tag>
+            </div>
+
+            <div class="account-card__stats">
+              <div class="stat" :class="{ 'stat--low': isLowBalance(record) }">
+                <span class="stat__label">当前余额</span>
+                <span class="stat__value">¥{{ record.balance }}</span>
+              </div>
+              <div class="stat">
+                <span class="stat__label">提醒阈值</span>
+                <span class="stat__value">¥{{ record.lowest }}</span>
+              </div>
+            </div>
+
+            <div class="account-card__meta">
+              <span><icon-email /> 通知邮箱:{{ record.email }}</span>
+              <span><icon-clock-circle /> 更新时间:{{ formatTime(record.update_time) }}</span>
+            </div>
+
+            <p v-if="record.notes" class="account-card__notes">{{ record.notes }}</p>
+
+            <div class="account-card__actions">
+              <a-button size="small" type="outline" @click="GetChangeRecord(record.id)">
+                <template #icon><icon-select-all /></template>
+                账单记录
+              </a-button>
+              <a-dropdown :popup-max-height="false" trigger="hover">
+                <a-button size="small" type="text">
+                  更多
+                  <icon-down />
+                </a-button>
+                <template #content>
+                  <a-doption @click="editAccount(record)"><icon-edit /> 编辑任务</a-doption>
+                  <a-doption @click="DeleteAccount(record)"><icon-delete /> 删除任务</a-doption>
+                </template>
+              </a-dropdown>
+            </div>
+          </article>
+        </div>
+      </a-spin>
+    </div>
   </div>
 
-  <!-- 账号编辑对话框 -->
-  <a-modal v-model:visible="visible" title="编辑电费提醒任务" @cancel="handleCancel" @before-ok="handleBeforeOk" draggable
-    :ok-loading="ok_loading" esc-to-close closable>
-    <a-form :model="form">
+  <a-modal
+    v-model:visible="visible"
+    :title="form.id ? '编辑电费提醒任务' : '添加电费提醒任务'"
+    class="power-modal"
+    @cancel="handleCancel"
+    @before-ok="handleBeforeOk"
+    draggable
+    :ok-loading="ok_loading"
+    esc-to-close
+    closable
+  >
+    <a-form :model="form" layout="vertical" class="power-form">
       <a-form-item field="area" label="校区" :loading="selectLoading">
-        <a-select v-model="form.area" placeholder="请选择所在校区" default-value="" :options="areas"
-          @change="GetPowerData('dylist', form.area)" />
+        <a-select
+          v-model="form.area"
+          placeholder="请选择所在校区"
+          :options="areas"
+          @change="GetPowerData('dylist', form.area)"
+        />
       </a-form-item>
       <a-form-item field="building" label="楼栋" :loading="selectLoading">
-        <a-select v-model="form.building" placeholder="请选择所在楼栋" default-value="" :options="buildings"
-          @change="GetPowerData('mphlist', form.building)" />
+        <a-select
+          v-model="form.building"
+          placeholder="请选择所在楼栋"
+          :options="buildings"
+          @change="GetPowerData('mphlist', form.building)"
+        />
       </a-form-item>
       <a-form-item field="room" label="宿舍号" :loading="selectLoading">
-        <a-select v-model="form.room" placeholder="请选择所在宿舍" default-value="" :options="rooms" />
+        <a-select v-model="form.room" placeholder="请选择所在宿舍" :options="rooms" />
       </a-form-item>
-      <a-form-item field="lowest" label="提醒阈值">
-        <a-input-number v-model="form.lowest" placeholder="请选择提醒阈值" :step="1" :precision="2" />
+      <a-form-item field="lowest" label="提醒阈值(元)">
+        <a-input-number v-model="form.lowest" placeholder="余额低于该值时发送邮件" :step="1" :precision="2" />
       </a-form-item>
       <a-form-item field="email" label="通知邮箱">
         <EmailAutoComplete v-model="form.email" placeholder="用于接收电费变更通知" allow-clear />
       </a-form-item>
       <a-form-item field="notes" label="备注">
-        <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
-          show-word-limit />
+        <a-textarea
+          v-model="form.notes"
+          no-trim
+          placeholder="添加对账号的备注(非必填)"
+          :max-length="{ length: 50 }"
+          allow-clear
+          show-word-limit
+        />
       </a-form-item>
     </a-form>
   </a-modal>
 
-  <!-- 电费变更记录 -->
-  <a-modal v-model:visible="listVisible" title="电费变更记录" esc-to-close closable width="auto" hide-cancel>
-    <a-table :bordered="false" :data="changeList" :columns="listColumns" stripe hoverable :loading="listLoading" :pagination="{
-      showPageSize: true,
-      showJumper: true,
-      showTotal: true,
-      pageSize: pagination.pagesize,
-      current: pagination.current,
-      total: pagination.total
-    }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
+  <a-modal
+    v-model:visible="listVisible"
+    title="电费变更记录"
+    class="power-modal power-modal--wide"
+    esc-to-close
+    closable
+    width="auto"
+    hide-cancel
+  >
+    <a-table
+      :bordered="false"
+      :data="changeList"
+      :columns="listColumns"
+      stripe
+      hoverable
+      :loading="listLoading"
+      class="change-table"
+      :pagination="{
+        showPageSize: true,
+        showJumper: true,
+        showTotal: true,
+        pageSize: pagination.pagesize,
+        current: pagination.current,
+        total: pagination.total
+      }"
+      @page-change="handlePageChange"
+      @page-size-change="handlePageSizeChange"
+    >
       <template #balance="{ record }">
         ¥{{ record.balance }}
       </template>
@@ -98,12 +163,12 @@
         ¥{{ record.old_balance }}
       </template>
       <template #change="{ record }">
-        <a-tag :color="Number(record.balance) < Number(record.old_balance) ? 'red' : 'green'">¥{{ Number(record.balance
-          - record.old_balance).toFixed(2)
-        }}</a-tag>
+        <a-tag :color="Number(record.balance) < Number(record.old_balance) ? 'red' : 'green'">
+          ¥{{ (Number(record.balance) - Number(record.old_balance)).toFixed(2) }}
+        </a-tag>
       </template>
       <template #time="{ record }">
-        {{ stramptoTime(record.time) }}
+        {{ formatTime(record.time) }}
       </template>
     </a-table>
   </a-modal>
@@ -115,18 +180,22 @@ import { getPowerData, addAccount, deleteAccount, getAccount, getChangeRecord }
 import { Modal, Notification, Message } from '@arco-design/web-vue'
 import { useRoute } from 'vue-router'
 import { getNotice } from '@/utils/util'
+import { formatStoreTimeFull } from '@/utils/storeFormat'
 
 const notice = ref('')
 
 const GetNotice = async () => {
   const { path } = useRoute()
-  const res = await getNotice(path)
-  notice.value = res
+  notice.value = await getNotice(path)
 }
 
 const data = ref([])
 const loading = ref(false)
 
+const formatTime = (time) => formatStoreTimeFull(time)
+
+const isLowBalance = (record) => Number(record.balance) <= Number(record.lowest)
+
 const listVisible = ref(false)
 const pagination = reactive({
   total: 0,
@@ -134,15 +203,13 @@ const pagination = reactive({
   pagesize: 15
 })
 
-// 分页 - 页码变化
 const handlePageChange = (page) => {
   pagination.current = page
 }
 
-// 分页 - 每页条数变化
 const handlePageSizeChange = (size) => {
   pagination.pagesize = size
-  pagination.current = 1 // 页大小变化后回到第一页
+  pagination.current = 1
 }
 
 const changeList = ref([])
@@ -154,16 +221,17 @@ const GetChangeRecord = async (id) => {
     listVisible.value = true
     changeList.value = []
     const res = await getChangeRecord({ id })
-    if (!res || res.code !== 0)
+    if (!res || res.code !== 0) {
       return Notification.error({
-        title: '获取电费变更记录失败',
+        title: '获取电费变更记录失败',
         content: res?.msg ?? '请稍后再试'
       })
+    }
     changeList.value = res.data
     pagination.total = res.data.length
   } catch (error) {
     Notification.error({
-      title: '获取电费变更记录失败',
+      title: '获取电费变更记录失败',
       content: error.message || '请稍后再试'
     })
   } finally {
@@ -180,71 +248,36 @@ const form = reactive({
   room: '',
   email: '',
   notes: '',
-  lowest: 10.00
+  lowest: 10.0
 })
 
-const columns = [{
-  title: '校区',
-  dataIndex: 'area',
-  width: 120
-}, {
-  title: '楼栋',
-  dataIndex: 'building',
-  width: 130
-}, {
-  title: '寝室号',
-  dataIndex: 'room',
-}, {
-  title: '通知邮箱',
-  dataIndex: 'email',
-  width: 220
-}, {
-  title: '当前余额',
-  slotName: 'balance',
-  width: 120
-}, {
-  title: '触发提醒阈值',
-  slotName: 'lowest',
-  width: 120
-}, {
-  title: '扣费日期',
-  dataIndex: 'koufei_date',
-  width: 170
-}, {
-  title: '上次更新时间',
-  slotName: 'update_time',
-  width: 170
-}, {
-  title: '备注',
-  dataIndex: 'notes'
-}, {
-  title: '操作',
-  slotName: 'optional',
-  width: 200,
-  fixed: 'right'
-}]
-
-const listColumns = [{
-  title: '记录时间',
-  slotName: 'time',
-  width: 170
-}, {
-  title: '扣费时间',
-  dataIndex: 'change_time',
-  width: 170
-}, {
-  title: '电费余额',
-  slotName: 'balance',
-  width: 120
-}, {
-  title: '原余额',
-  slotName: 'old_balance',
-  width: 120
-}, {
-  title: '变动金额',
-  slotName: 'change',
-  width: 120
-}]
+const listColumns = [
+  {
+    title: '记录时间',
+    slotName: 'time',
+    width: 170
+  },
+  {
+    title: '扣费时间',
+    dataIndex: 'change_time',
+    width: 170
+  },
+  {
+    title: '电费余额',
+    slotName: 'balance',
+    width: 120
+  },
+  {
+    title: '原余额',
+    slotName: 'old_balance',
+    width: 120
+  },
+  {
+    title: '变动金额',
+    slotName: 'change',
+    width: 120
+  }
+]
 
 const selectLoading = ref(false)
 const areas = ref([])
@@ -253,17 +286,15 @@ const rooms = ref([])
 
 const GetPowerData = async (type, pid = '') => {
   try {
-    const data = {
-      type,
-      pid
-    }
-    const res = await getPowerData(data)
-    if (!res || res.code !== 0)
+    const payload = { type, pid }
+    const res = await getPowerData(payload)
+    if (!res || res.code !== 0) {
       return Notification.error({
-        title: '获取电费信息失败',
+        title: '获取电费信息失败',
         content: res?.msg ?? '请稍后再试'
       })
-    let resdata = res.data.map(item => ({
+    }
+    const resdata = res.data.map((item) => ({
       value: item,
       label: item
     }))
@@ -283,7 +314,7 @@ const GetPowerData = async (type, pid = '') => {
     }
   } catch (error) {
     Notification.error({
-      title: '获取电费信息失败',
+      title: '获取电费信息失败',
       content: error.message || '请稍后再试'
     })
   }
@@ -297,7 +328,9 @@ const editAccount = (item) => {
     form.room = item.room
     form.email = item.email
     form.notes = item.notes
-    form.lowest = Number(item.lowest) ?? 10.00
+    form.lowest = Number(item.lowest) ?? 10.0
+    if (form.area) GetPowerData('dylist', form.area)
+    if (form.building) GetPowerData('mphlist', form.building)
   } else {
     form.id = null
     form.area = ''
@@ -305,7 +338,7 @@ const editAccount = (item) => {
     form.room = ''
     form.email = ''
     form.notes = ''
-    form.lowest = 10.00
+    form.lowest = 10.0
   }
   visible.value = true
 }
@@ -325,25 +358,21 @@ const handleBeforeOk = async (done) => {
       return false
     }
 
-    let data = {
-      ...form
-    }
-
-    const res = await addAccount(data)
+    const res = await addAccount({ ...form })
     if (!res || res.code !== 0) {
       Notification.error({
-        title: '保存电费提醒任务失败',
+        title: '保存电费提醒任务失败',
         content: res?.msg ?? '请稍后再试'
       })
       return false
     }
 
-    Message.success('保存成功')
+    Message.success('保存成功')
     done()
     getAccounts()
   } catch (error) {
     Notification.error({
-      title: '保存电费提醒任务失败',
+      title: '保存电费提醒任务失败',
       content: error.message || '请稍后再试'
     })
     return false
@@ -360,15 +389,16 @@ const getAccounts = async () => {
   try {
     loading.value = true
     const res = await getAccount()
-    if (!res || res.code !== 0)
+    if (!res || res.code !== 0) {
       return Notification.error({
-        title: '获取账号列表失败',
+        title: '获取账号列表失败',
         content: res?.msg ?? '请稍后再试'
       })
+    }
     data.value = res.data
   } catch (error) {
     Notification.error({
-      title: '获取账号列表失败',
+      title: '获取账号列表失败',
       content: error.message || '请稍后再试'
     })
   } finally {
@@ -379,79 +409,194 @@ const getAccounts = async () => {
 const DeleteAccount = async (item) => {
   Modal.confirm({
     title: '删除任务',
-    content: () => h('div', [
-      h('p', '您是否要删除该电费提醒任务?该操作不可逆')
-    ]),
+    content: () =>
+      h('div', [h('p', '您是否要删除该电费提醒任务?该操作不可逆')]),
     onOk: async () => {
       const res = await deleteAccount({ id: item.id })
-      if (!res || res.code !== 0)
+      if (!res || res.code !== 0) {
         return Notification.error({
           title: '删除失败',
           content: res?.msg ?? '请稍后再试'
         })
-      Message.success('删除成功!')
+      }
+      Message.success('删除成功')
       getAccounts()
     }
   })
 }
 
-const stramptoTime = (time) => {
-  return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
-}
-
 onMounted(() => {
   GetPowerData('buildlist')
   getAccounts()
   GetNotice()
 })
-
 </script>
 
-<style scoped lang="less">
-.container {
-  padding: 0 20px 20px 20px;
+<style lang="less" scoped>
+@import '@/styles/store-theme.less';
+
+.page-header {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+.primary-btn {
+  border-radius: 999px;
+  background: @store-primary !important;
+  border-color: @store-primary !important;
 }
 
-.table {
-  margin-top: 15px;
+.notice {
+  margin-bottom: 16px;
+  border-radius: @store-radius-sm;
+}
 
-  .state {
+.account-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 100%;
+}
+
+.account-card {
+  width: 100%;
+  box-sizing: border-box;
+  background: @store-card-bg;
+  border: 1px solid @store-card-border;
+  border-radius: @store-radius;
+  padding: 16px 20px;
+  transition: box-shadow 0.2s, border-color 0.2s;
+
+  &:hover {
+    box-shadow: @store-shadow;
+    border-color: fade(@store-accent, 40%);
+  }
+
+  &--low {
+    border-color: rgba(245, 63, 63, 0.35);
+    background: linear-gradient(135deg, #fff 0%, #fff8f6 100%);
+  }
+
+  &__head {
     display: flex;
-    align-items: center;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 12px;
+    margin-bottom: 14px;
+  }
 
-    .circle {
-      border-radius: 50%;
-      height: 8px;
-      min-height: 8px;
-      width: 8px;
-      min-width: 8px;
-      margin-right: 5px;
+  &__location {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 1.05rem;
+    font-weight: 600;
+    color: @store-primary;
+
+    .loc-icon {
+      color: @store-accent;
+      font-size: 1.1rem;
+      flex-shrink: 0;
     }
+  }
 
-    .zero {
-      background-color: rgb(var(--orange-6));
+  &__stats {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    margin-bottom: 12px;
+
+    @media (min-width: 520px) {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
     }
+  }
 
-    .one {
-      background-color: rgb(var(--green-6));
+  &__meta {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16px;
+    font-size: 0.85rem;
+    color: @store-text-muted;
+
+    span {
+      display: inline-flex;
+      align-items: center;
+      gap: 4px;
     }
+  }
+
+  &__notes {
+    margin: 10px 0 0;
+    font-size: 0.85rem;
+    color: @store-text-muted;
+    padding: 8px 12px;
+    background: @store-bg;
+    border-radius: @store-radius-sm;
+  }
+
+  &__actions {
+    margin-top: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    gap: 8px;
+  }
+}
 
-    .else {
-      background-color: rgb(var(--red-6));
+.stat {
+  padding: 10px 12px;
+  background: @store-bg;
+  border-radius: @store-radius-sm;
+
+  &__label {
+    display: block;
+    font-size: 0.75rem;
+    color: @store-text-muted;
+    margin-bottom: 4px;
+  }
+
+  &__value {
+    font-size: 0.95rem;
+    font-weight: 600;
+    color: @store-primary;
+  }
+
+  &--low {
+    background: #fff5f5;
+
+    .stat__value {
+      color: rgb(var(--red-6));
     }
   }
 }
+</style>
 
-.custom-filter {
-  padding: 20px;
-  background: var(--color-bg-5);
-  border: 1px solid var(--color-neutral-3);
-  border-radius: var(--border-radius-medium);
-  box-shadow: 0 2px 5px rgb(0 0 0 / 10%);
+<style lang="less">
+@import '@/styles/store-theme.less';
+
+.power-modal {
+  .arco-modal-header {
+    border-bottom: 1px solid @store-card-border;
+  }
+
+  .arco-btn-primary {
+    background: @store-primary;
+    border-color: @store-primary;
+  }
 }
 
-.custom-filter-footer {
-  display: flex;
-  justify-content: space-between;
+.power-modal--wide .arco-modal-body {
+  max-width: min(92vw, 720px);
+}
+
+.change-table {
+  .arco-table-th {
+    background: @store-bg;
+    color: @store-primary;
+  }
 }
-</style>
+</style>