Browse Source

🎈 perf: 优化页面展示效果

Pchen. 3 weeks ago
parent
commit
7ee972ccb1
34 changed files with 621 additions and 170 deletions
  1. 9 0
      src/api/order.js
  2. 4 17
      src/components/LepaoAccountCard/accountDetailCard.vue
  3. 2 0
      src/components/index.js
  4. 48 0
      src/components/layout/AppQueryFilter.vue
  5. 20 5
      src/components/userCard/userCard.vue
  6. 51 0
      src/hooks/useResponsiveTable.js
  7. 6 4
      src/layout/default-layout.vue
  8. 1 1
      src/pages/User/setting/components/basic-information.vue
  9. 4 1
      src/pages/admin/goods/couponList.vue
  10. 5 1
      src/pages/admin/goods/goodsList.vue
  11. 8 10
      src/pages/admin/goods/orderList.vue
  12. 3 1
      src/pages/admin/goods/sendCountRequestList.vue
  13. 7 1
      src/pages/admin/lepaoAccount/accountList.vue
  14. 2 0
      src/pages/admin/lepaoBindAudit/index.vue
  15. 7 1
      src/pages/admin/lepaoCountLedger/index.vue
  16. 4 1
      src/pages/admin/lepaoProxy/index.vue
  17. 6 1
      src/pages/admin/lepaoRecords/lepaoRecords.vue
  18. 3 1
      src/pages/admin/mqQueue/index.vue
  19. 3 1
      src/pages/admin/reqLog/index.vue
  20. 131 45
      src/pages/admin/user/userList.vue
  21. 26 13
      src/pages/admin/workOrder/orderDetail.vue
  22. 7 1
      src/pages/admin/workOrder/orderList.vue
  23. 13 3
      src/pages/lepao/accountList/index.vue
  24. 7 1
      src/pages/lepao/countLedger/index.vue
  25. 6 1
      src/pages/lepao/lepaoRecords/index.vue
  26. 3 1
      src/pages/path/pathList.vue
  27. 16 33
      src/pages/power/accountList.vue
  28. 1 1
      src/pages/qxs/getBookList.vue
  29. 24 10
      src/pages/store/goodsDetail/index.vue
  30. 65 5
      src/pages/store/orders/orderDetail/index.vue
  31. 53 10
      src/pages/store/orders/orderList/index.vue
  32. 2 0
      src/pages/store/sendCountRecords/index.vue
  33. 7 0
      src/style.css
  34. 67 0
      src/styles/store-theme.less

+ 9 - 0
src/api/order.js

@@ -2,6 +2,7 @@ import request from '../utils/request'
 
 const api = {
   Create: '/Order/CreateOrder',
+  Cancel: '/Order/CancelOrder',
   Detail: '/Order/Detail',
   GetMyOrder: '/Order/GetMyOrders',
   AdminOrderList: '/Admin/Order/List',
@@ -24,6 +25,14 @@ export function orderDeatil(parameter) {
   })
 }
 
+export function cancelOrder(parameter) {
+  return request({
+    url: api.Cancel,
+    method: 'post',
+    data: parameter
+  })
+}
+
 export function getMyOrder(parameter) {
   return request({
     url: api.GetMyOrder,

+ 4 - 17
src/components/LepaoAccountCard/accountDetailCard.vue

@@ -103,7 +103,7 @@
                 <a-table-column title="乐跑时间" :width="160">
                   <template #cell="{ record }">{{ formatTime(record.time) }}</template>
                 </a-table-column>
-                <a-table-column title="" :width="90" fixed="right">
+                <a-table-column title="" :width="90" :fixed="tableFixed('right')">
                   <template #cell="{ record }">
                     <a-button type="text" size="small" @click="openPlatformRecordDetail(record)">详情</a-button>
                   </template>
@@ -166,7 +166,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { ref, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import {
   lepaoRecords,
@@ -176,6 +176,7 @@ import {
 } from '@/api/lepao'
 import { Notification, Message } from '@arco-design/web-vue'
 import useChartOption from '@/hooks/chart-option'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 
 const props = defineProps({
   admin: {
@@ -214,23 +215,9 @@ const officialSummary = ref({})
 const officialPagination = ref({ current: 1, pageSize: 10, total: 0 })
 const recordTab = ref('platform')
 const calendarYear = ref(new Date().getFullYear())
-const isMobile = ref(false)
+const { isMobile, tableFixed } = useResponsiveTable()
 const router = useRouter()
 
-let mobileResizeHandler = null
-
-onMounted(() => {
-  mobileResizeHandler = () => {
-    isMobile.value = window.innerWidth <= 768
-  }
-  mobileResizeHandler()
-  window.addEventListener('resize', mobileResizeHandler)
-})
-
-onUnmounted(() => {
-  if (mobileResizeHandler) window.removeEventListener('resize', mobileResizeHandler)
-})
-
 const modalTitle = computed(() => {
   if (!account.value) return '账号详情'
   const name = account.value.name || account.value.student_num

+ 2 - 0
src/components/index.js

@@ -17,6 +17,7 @@ import Chart from './Chart/index.vue'
 import Breadcrumb from './Breadcrumb/index.vue'
 import EmailAutoComplete from './EmailAutoComplete/index.vue'
 import AppPageShell from './layout/AppPageShell.vue'
+import AppQueryFilter from './layout/AppQueryFilter.vue'
 
 use([
   CanvasRenderer,
@@ -43,5 +44,6 @@ export default {
     Vue.component('Breadcrumb', Breadcrumb)
     Vue.component('EmailAutoComplete', EmailAutoComplete)
     Vue.component('AppPageShell', AppPageShell)
+    Vue.component('AppQueryFilter', AppQueryFilter)
   },
 }

+ 48 - 0
src/components/layout/AppQueryFilter.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="app-query-filter" :class="{ 'is-expanded': expanded }">
+    <a-button
+      v-show="isMobile"
+      type="outline"
+      long
+      class="app-query-filter__toggle"
+      @click="expanded = !expanded"
+    >
+      <icon-filter />
+      <span>{{ label }}</span>
+      <icon-down class="app-query-filter__chevron" />
+    </a-button>
+    <div v-if="showPanel" class="app-query-filter__panel">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+
+defineProps({
+  label: { type: String, default: '筛选条件' }
+})
+
+const expanded = ref(false)
+const isMobile = ref(false)
+
+let mediaQuery = null
+
+const syncMobile = () => {
+  isMobile.value = mediaQuery?.matches ?? window.innerWidth <= 768
+  if (!isMobile.value) expanded.value = false
+}
+
+const showPanel = computed(() => !isMobile.value || expanded.value)
+
+onMounted(() => {
+  mediaQuery = window.matchMedia('(max-width: 768px)')
+  syncMobile()
+  mediaQuery.addEventListener('change', syncMobile)
+})
+
+onUnmounted(() => {
+  mediaQuery?.removeEventListener('change', syncMobile)
+})
+</script>

+ 20 - 5
src/components/userCard/userCard.vue

@@ -27,7 +27,7 @@
           class="action-btn action-btn--primary"
           @click="$router.push('/lepao/accountList')"
         >
-          <icon-thunderbolt /> 去乐跑
+          <icon-thunderbolt />&nbsp;去乐跑
         </a-button>
         <a-button
           v-if="hasPermission('action.goods.sendCount')"
@@ -35,10 +35,10 @@
           class="action-btn"
           @click="SendCount"
         >
-          <icon-gift /> 赠送
+          <icon-gift />&nbsp;赠送
         </a-button>
-        <a-button size="large" class="action-btn" @click="goSendCountRecords">
-          <icon-list /> 记录
+        <a-button size="large" class="action-btn" @click="goCountLedger">
+          <icon-list />&nbsp;明细
         </a-button>
       </div>
     </div>
@@ -65,6 +65,11 @@
         <a-input-number v-model="sendform.count" :min="1" :max="9999" mode="button" />
       </a-form-item>
     </a-form>
+    <div v-if="hasPermission('action.goods.sendCount')" class="send-modal-extra">
+      <a-link @click="goSendCountRecords">
+        <icon-list />&nbsp;赠送记录
+      </a-link>
+    </div>
   </a-modal>
 </template>
 
@@ -126,7 +131,12 @@ const SendCount = () => {
   visible.value = true
 }
 
-const goSendCountRecords = () => router.push('/store/sendCountRecords')
+const goCountLedger = () => router.push('/lepao/countLedger')
+
+const goSendCountRecords = () => {
+  visible.value = false
+  router.push('/store/sendCountRecords')
+}
 
 const GetCount = async () => {
   try {
@@ -202,4 +212,9 @@ onUnmounted(stopPolling)
     border-color: @store-primary !important;
   }
 }
+
+.send-modal-extra {
+  margin-top: 4px;
+  text-align: center;
+}
 </style>

+ 51 - 0
src/hooks/useResponsiveTable.js

@@ -0,0 +1,51 @@
+import { ref, computed, onMounted, onUnmounted, unref } from 'vue'
+
+/** 与全局响应式断点一致:≤768px 视为移动端 */
+export const TABLE_MOBILE_BREAKPOINT = 768
+
+/** 移除列配置中的 fixed,用于移动端 */
+export function stripTableFixed(columns) {
+  if (!Array.isArray(columns)) return columns
+  return columns.map((col) => {
+    if (!col?.fixed) return col
+    const { fixed, ...rest } = col
+    return rest
+  })
+}
+
+/**
+ * 表格响应式:桌面端保留固定列,移动端取消固定列
+ */
+export function useResponsiveTable(breakpoint = TABLE_MOBILE_BREAKPOINT) {
+  const isMobile = ref(false)
+  let resizeHandler = null
+
+  const syncMobile = () => {
+    isMobile.value = window.innerWidth <= breakpoint
+  }
+
+  onMounted(() => {
+    resizeHandler = syncMobile
+    syncMobile()
+    window.addEventListener('resize', resizeHandler)
+  })
+
+  onUnmounted(() => {
+    if (resizeHandler) {
+      window.removeEventListener('resize', resizeHandler)
+    }
+  })
+
+  /** 供 a-table-column 的 :fixed 绑定 */
+  const tableFixed = (side = 'right') => (isMobile.value ? undefined : side)
+
+  /** 供 :columns 绑定,根据 isMobile 自动去掉 fixed */
+  const useTableColumns = (columnsSource) =>
+    computed(() => {
+      const cols =
+        typeof columnsSource === 'function' ? columnsSource() : unref(columnsSource)
+      return isMobile.value ? stripTableFixed(cols) : cols
+    })
+
+  return { isMobile, tableFixed, useTableColumns, stripTableFixed }
+}

+ 6 - 4
src/layout/default-layout.vue

@@ -120,7 +120,11 @@ onMounted(() => {
     button.addEventListener('mousedown', handleMouseDown)
   }
   eventBus.on('closeAI', () => { chat.value = false })
-  syncSiderCollapsed()
+  const mobile = window.innerWidth <= 992
+  isMobile.value = mobile
+  if (mobile) {
+    siderCollapsed.value = false
+  }
   window.addEventListener('resize', syncSiderCollapsed)
   getPopup()
 })
@@ -163,9 +167,7 @@ const handlePopupClose = async () => {
 const syncSiderCollapsed = () => {
   const mobile = window.innerWidth <= 992
   isMobile.value = mobile
-  if (mobile) {
-    siderCollapsed.value = true
-  } else {
+  if (!mobile) {
     siderCollapsed.value = false
   }
 }

+ 1 - 1
src/pages/User/setting/components/basic-information.vue

@@ -18,7 +18,7 @@
     <a-divider />
 
     <div class="email">
-      <a-form size="large" ref="emailFormRef" :model="emailForm" class="form" @submit="changeEmail" >
+      <a-form size="large" ref="emailFormRef" :model="emailForm" class="form" @submit="changeEmail">
         <a-form-item field="email" label="邮箱" :rules="[{ type: 'email', required: true, message: '请填写正确的邮箱地址' }]"
           :validate-trigger="['change']">
           <EmailAutoComplete placeholder="请输入邮箱" allow-clear v-model="emailForm.email">

+ 4 - 1
src/pages/admin/goods/couponList.vue

@@ -72,7 +72,7 @@
           <a-table-column title="有效期" :width="200">
             <template #cell="{ record }">{{ formatValidity(record) }}</template>
           </a-table-column>
-          <a-table-column title="操作" :width="100" fixed="right">
+          <a-table-column title="操作" :width="100" :fixed="tableFixed('right')">
             <template #cell="{ record }">
               <a-button type="text" size="small" @click="goEdit(record.id)">编辑</a-button>
             </template>
@@ -89,6 +89,9 @@ import { useRouter } from 'vue-router'
 import { adminCouponList } from '@/api/coupon'
 import { Notification } from '@arco-design/web-vue'
 import { formatStoreTime } from '@/utils/storeFormat'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+
+const { tableFixed } = useResponsiveTable()
 
 const router = useRouter()
 const loading = ref(false)

+ 5 - 1
src/pages/admin/goods/goodsList.vue

@@ -84,6 +84,7 @@
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 import { adminGetGoodsList } from '@/api/goods'
 import { Notification } from '@arco-design/web-vue'
 import { formatStoreTimeFull } from '@/utils/storeFormat'
@@ -94,7 +95,7 @@ const queryData = reactive({ keyword: '' })
 const loading = ref(false)
 const data = ref([])
 
-const columns = [
+const columnDefs = [
     { title: 'ID', dataIndex: 'id', width: 70 },
     { title: '图标', slotName: 'icon', width: 70 },
     { title: '商品名称', dataIndex: 'name', ellipsis: true, tooltip: true },
@@ -109,6 +110,9 @@ const columns = [
   { title: '操作', slotName: 'optional', fixed: 'right', width: 90 }
 ]
 
+const { useTableColumns } = useResponsiveTable()
+const columns = useTableColumns(columnDefs)
+
 const search = () => {
   pagination.current = 1
   getList()

+ 8 - 10
src/pages/admin/goods/orderList.vue

@@ -3,9 +3,8 @@
     <Breadcrumb />
 
     <a-card :bordered="false" class="page-card" title="订单管理">
-      <a-collapse :default-active-key="['filter']" :bordered="false" class="filter-collapse">
-        <a-collapse-item key="filter" header="筛选条件">
-          <a-form :model="queryData" layout="vertical" class="filter-grid">
+      <AppQueryFilter>
+          <a-form :model="queryData" layout="vertical" class="filter-grid app-query-form">
             <a-row :gutter="16">
               <a-col :xs="24" :sm="12" :md="8" :lg="6">
                 <a-form-item label="订单号">
@@ -65,8 +64,7 @@
               </a-button>
             </a-space>
           </a-form>
-        </a-collapse-item>
-      </a-collapse>
+      </AppQueryFilter>
 
       <a-table
         :bordered="false"
@@ -126,6 +124,7 @@
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 import { useRouter } from 'vue-router'
 import { adminOrderList } from '@/api/order'
 import { Notification } from '@arco-design/web-vue'
@@ -163,7 +162,7 @@ const queryData = reactive({
 const loading = ref(false)
 const data = ref([])
 
-const columns = [
+const columnDefs = [
   { title: '订单号', dataIndex: 'orderId', width: 180, ellipsis: true, tooltip: true },
   { title: '商品', dataIndex: 'goods_name', width: 140, ellipsis: true, tooltip: true },
   { title: '用户', slotName: 'username', width: 150 },
@@ -176,6 +175,9 @@ const columns = [
   { title: '操作', slotName: 'optional', fixed: 'right', width: 80 }
 ]
 
+const { useTableColumns } = useResponsiveTable()
+const columns = useTableColumns(columnDefs)
+
 const getStateLabel = (state) => getOrderStateMeta(state).label
 const getStateColor = (state) => getOrderStateMeta(state).color
 
@@ -250,10 +252,6 @@ onMounted(() => {
   border-radius: 12px;
 }
 
-.filter-collapse {
-  margin-bottom: 16px;
-}
-
 .filter-grid {
   padding-top: 8px;
 }

+ 3 - 1
src/pages/admin/goods/sendCountRequestList.vue

@@ -3,7 +3,8 @@
     <Breadcrumb />
 
     <a-card title="赠送审核列表">
-      <a-row>
+      <AppQueryFilter>
+      <a-row class="query-row app-query-form">
         <a-col :flex="1">
           <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
             <a-row :gutter="16">
@@ -33,6 +34,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-table :bordered="false"
         :data="data"

+ 7 - 1
src/pages/admin/lepaoAccount/accountList.vue

@@ -3,6 +3,7 @@
         <Breadcrumb />
 
         <a-card title="乐跑账号管理">
+            <AppQueryFilter>
             <a-row class="query-row">
                 <a-col :flex="'1000px'" class="query-main">
                     <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -96,6 +97,7 @@
                     </a-space>
                 </a-col>
             </a-row>
+            </AppQueryFilter>
 
             <a-table :data="data" :bordered="false" hoverable class="table table-clickable" :loading="loading" :columns="columns" :scroll="{ x: 2850 }" @row-click="onRowClick" :pagination="{
                 showPageSize: true,
@@ -306,6 +308,7 @@ import bindBot from '@/components/BindBot/bindBot.vue'
 import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.vue'
 import { getSemesterTimestamps } from '@/utils/util'
 import { hasPermission } from '@/utils/permission'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 
 const faceRecoRef = ref(null)
 const bindBotRef = ref(null)
@@ -380,7 +383,7 @@ const state = [
 ]
 const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
 
-const columns = [
+const columnDefs = [
     {
         title: 'ID',
         dataIndex: 'id',
@@ -471,6 +474,9 @@ const columns = [
         width: 90
     }]
 
+const { useTableColumns } = useResponsiveTable()
+const columns = useTableColumns(columnDefs)
+
 const bindAuditColumns = [
     { title: '学号', dataIndex: 'student_num', width: 140 },
     { title: '乐跑账号', slotName: 'lepao_user', width: 180 },

+ 2 - 0
src/pages/admin/lepaoBindAudit/index.vue

@@ -2,6 +2,7 @@
   <div class="store-page">
     <Breadcrumb />
     <a-card title="绑定解绑审计">
+      <AppQueryFilter>
       <a-row class="query-row">
         <a-col :flex="'1000px'" class="query-main">
           <a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
@@ -60,6 +61,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-table :bordered="false"
         class="table"

+ 7 - 1
src/pages/admin/lepaoCountLedger/index.vue

@@ -3,6 +3,7 @@
     <Breadcrumb />
 
     <a-card title="乐跑次数明细">
+      <AppQueryFilter>
       <a-row class="queryForm app-query-form">
         <a-col :flex="1">
           <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
@@ -62,6 +63,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-table
         :data="data"
@@ -261,8 +263,12 @@ onMounted(() => {
 </script>
 
 <style scoped lang="less">
+@import '@/styles/store-theme.less';
+
 .queryForm {
-  margin-bottom: 16px;
+  @media (min-width: (@app-breakpoint-md + 1px)) {
+    margin-bottom: 16px;
+  }
 }
 
 .summary-muted {

+ 4 - 1
src/pages/admin/lepaoProxy/index.vue

@@ -180,7 +180,7 @@
                 <div class="summary-text">{{ record.summary }}</div>
               </template>
             </a-table-column>
-            <a-table-column title="操作" fixed="right" :width="88">
+            <a-table-column title="操作" :fixed="tableFixed('right')" :width="88">
               <template #cell="{ record }">
                 <a-popconfirm content="删除该条日志?" @ok="deleteOneLog(record.id)">
                   <a-button type="text" size="mini" status="danger">删除</a-button>
@@ -197,6 +197,7 @@
 <script setup>
 import { reactive, ref, computed, watch, onMounted } from 'vue'
 import { Notification, Modal } from '@arco-design/web-vue'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 import {
   getAdminLepaoProxyStatus,
   postAdminLepaoProxyConfig,
@@ -205,6 +206,8 @@ import {
   getAdminLepaoProxyResources
 } from '@/api/lepao'
 
+const { tableFixed } = useResponsiveTable()
+
 const pageLoading = ref(false)
 const logLoading = ref(false)
 const logDeleting = ref(false)

+ 6 - 1
src/pages/admin/lepaoRecords/lepaoRecords.vue

@@ -3,6 +3,7 @@
   <div class="store-page">
     <Breadcrumb />
     <a-card title="乐跑记录">
+      <AppQueryFilter>
       <a-row class="query-row">
         <a-col :flex="'1000px'" class="query-main">
           <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -56,6 +57,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading" :scroll="{ x: 1650 }" :pagination="{
         showPageSize: true,
@@ -151,7 +153,7 @@
               {{ stramptoTime(record.time) }}
             </template>
           </a-table-column>
-          <a-table-column title="" :width="100" fixed="right">
+          <a-table-column title="" :width="100" :fixed="tableFixed('right')">
             <template #cell="{ record }">
               <a-button @click="openDetail(record)">查看详情</a-button>
             </template>
@@ -170,6 +172,9 @@ import { adminLepaoRecords } from '@/api/lepao'
 import { Notification, Message } from '@arco-design/web-vue'
 import { IconSearch } from '@arco-design/web-vue/es/icon'
 import { getSemesterTimestamps } from '@/utils/util'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+
+const { tableFixed } = useResponsiveTable()
 
 const data = ref([])
 const loading = ref(false)

+ 3 - 1
src/pages/admin/mqQueue/index.vue

@@ -54,7 +54,8 @@
     </a-card>
 
     <a-card title="队列消息窥视(reject_requeue_true,不消费)" class="card-block">
-      <a-row :gutter="16" align="center">
+      <AppQueryFilter label="窥视条件">
+      <a-row :gutter="16" align="center" class="query-row app-query-form">
         <a-col :span="6">
           <a-form-item label="队列" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
             <a-select v-model="peekForm.queue" placeholder="选择队列">
@@ -80,6 +81,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-alert v-if="peekMeta.managementError" type="warning" style="margin-bottom: 12px">
         {{ peekMeta.managementError }}

+ 3 - 1
src/pages/admin/reqLog/index.vue

@@ -3,7 +3,8 @@
         <Breadcrumb />
 
         <a-card title="请求日志">
-            <a-row>
+            <AppQueryFilter>
+            <a-row class="query-row app-query-form">
                 <a-col :flex="1">
                     <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
                         label-align="left">
@@ -56,6 +57,7 @@
                     </a-space>
                 </a-col>
             </a-row>
+            </AppQueryFilter>
 
             <a-table :bordered="false" :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
                 :pagination="{

+ 131 - 45
src/pages/admin/user/userList.vue

@@ -3,6 +3,7 @@
         <Breadcrumb />
 
         <a-card title="用户管理">
+            <AppQueryFilter>
             <a-row class="query-row">
                 <a-col :flex="'1000px'" class="query-main">
                     <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -49,10 +50,12 @@
                     </a-space>
                 </a-col>
             </a-row>
+            </AppQueryFilter>
 
-            <a-table :data="data" :bordered="false" hoverable column-resizable class="table" :loading="loading"
-                :scroll="{ x: 1550 }"
-                :columns="columns" :pagination="{
+            <div class="table-wrap">
+            <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading"
+                :scroll="{ x: TABLE_SCROLL_X }"
+                :columns="tableColumns" :pagination="{
                     showPageSize: true,
                     showJumper: true,
                     showTotal: true,
@@ -61,11 +64,13 @@
                     total: pagination.total
                 }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
                 <template #username="{ record }">
-                    <a-avatar :size="35">
-                        <img :alt="record.username ?? ''"
-                            :src="record.avatar ?? 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
-                    </a-avatar>
-                    {{ record.username }}
+                    <div class="user-cell">
+                        <a-avatar :size="35" class="user-cell__avatar">
+                            <img :alt="record.username ?? ''"
+                                :src="record.avatar ?? 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
+                        </a-avatar>
+                        <span class="user-cell__name">{{ record.username || '-' }}</span>
+                    </div>
                 </template>
                 <template #registTime="{ record }">
                     {{ stramptoTime(record.registTime) }}
@@ -74,27 +79,23 @@
                     {{ stramptoTime(record.lastTime ?? record.registTime) }}
                 </template>
                 <template #qq_bind="{ record }">
-                    <template v-if="record.qq_social_nickname || record.qq_social_avatar">
-                        <a-space>
-                            <a-avatar :size="24">
-                                <img v-if="record.qq_social_avatar" :alt="record.qq_social_nickname || 'QQ'" :src="record.qq_social_avatar" />
-                                <icon-qq v-else />
-                            </a-avatar>
-                            <span>{{ record.qq_social_nickname || '未获取' }}</span>
-                        </a-space>
-                    </template>
+                    <div v-if="record.qq_social_nickname || record.qq_social_avatar" class="bind-cell">
+                        <a-avatar :size="24" class="bind-cell__avatar">
+                            <img v-if="record.qq_social_avatar" :alt="record.qq_social_nickname || 'QQ'" :src="record.qq_social_avatar" />
+                            <icon-qq v-else />
+                        </a-avatar>
+                        <span class="bind-cell__text">{{ record.qq_social_nickname || '未获取' }}</span>
+                    </div>
                     <span v-else class="muted">未绑定</span>
                 </template>
                 <template #wx_bind="{ record }">
-                    <template v-if="record.wx_social_nickname || record.wx_social_avatar">
-                        <a-space>
-                            <a-avatar :size="24">
-                                <img v-if="record.wx_social_avatar" :alt="record.wx_social_nickname || '微信'" :src="record.wx_social_avatar" />
-                                <icon-wechat v-else />
-                            </a-avatar>
-                            <span>{{ record.wx_social_nickname || '未获取' }}</span>
-                        </a-space>
-                    </template>
+                    <div v-if="record.wx_social_nickname || record.wx_social_avatar" class="bind-cell">
+                        <a-avatar :size="24" class="bind-cell__avatar">
+                            <img v-if="record.wx_social_avatar" :alt="record.wx_social_nickname || '微信'" :src="record.wx_social_avatar" />
+                            <icon-wechat v-else />
+                        </a-avatar>
+                        <span class="bind-cell__text">{{ record.wx_social_nickname || '未获取' }}</span>
+                    </div>
                     <span v-else class="muted">未绑定</span>
                 </template>
                 <template #last_login_type="{ record }">
@@ -125,6 +126,7 @@
                     </a-dropdown>
                 </template>
             </a-table>
+            </div>
         </a-card>
     </div>
 
@@ -218,6 +220,7 @@
 
 <script setup>
 import { ref, reactive, onMounted, computed } from 'vue'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 import { adminGetUserList, adminChangeLepaoCount, adminSetSendCountAutoApprove, adminSetUserBan } from '@/api/user'
 import { getPermissionPoints, getPermissionResources, getUserPermissions, setUserPermissions, updatePermissionResource } from '@/api/permission'
 import { Notification, Message, Modal } from '@arco-design/web-vue'
@@ -465,58 +468,88 @@ const handlePermissionBeforeOk = async (done) => {
     }
 }
 
-const columns = [
+const { useTableColumns } = useResponsiveTable()
+
+/** 列宽合计,移动端横向滚动查看全部字段 */
+const TABLE_SCROLL_X = 1650
 
+const columnDefs = [
     {
         title: 'UUID',
         dataIndex: 'uuid',
-        width: 160,
-    }, {
+        width: 200,
+        ellipsis: true,
+        tooltip: true
+    },
+    {
         title: '用户名',
         slotName: 'username',
-        width: 160,
-    }, {
+        width: 180,
+        ellipsis: true,
+        tooltip: true
+    },
+    {
         title: '邮箱',
         dataIndex: 'email',
-        width: 180,
+        width: 200,
+        ellipsis: true,
+        tooltip: true
     },
     {
         title: 'QQ 绑定',
         slotName: 'qq_bind',
-        width: 150,
+        width: 160,
+        ellipsis: true,
+        tooltip: true
     },
     {
         title: '微信绑定',
         slotName: 'wx_bind',
-        width: 150,
+        width: 160,
+        ellipsis: true,
+        tooltip: true
     },
     {
         title: '登录方式',
         slotName: 'last_login_type',
         width: 120,
-    }, {
+        ellipsis: true,
+        tooltip: true
+    },
+    {
         title: '注册时间',
         slotName: 'registTime',
-        width: 160,
-    }, {
+        width: 170,
+        ellipsis: true,
+        tooltip: true
+    },
+    {
         title: '上次登录时间',
         slotName: 'lastTime',
-        width: 160,
-    }, {
+        width: 170,
+        ellipsis: true,
+        tooltip: true
+    },
+    {
         title: '剩余乐跑次数',
         dataIndex: 'lepao_count',
-        width: 120,
-    }, {
+        width: 120
+    },
+    {
         title: '账号状态',
         slotName: 'is_banned_status',
-        width: 100,
-    }, {
+        width: 100
+    },
+    {
         title: '操作',
         slotName: 'optional',
         width: 120,
         align: 'center',
         fixed: 'right'
-    }]
+    }
+]
+
+const tableColumns = useTableColumns(columnDefs)
 
 const search = () => {
     pagination.current = 1
@@ -649,10 +682,63 @@ const formatLastLoginType = (type) => {
     width: 100%;
 }
 
-.table {
+.table-wrap {
+    width: 100%;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
     margin-top: 15px;
 }
 
+.table {
+    margin-top: 0;
+    min-width: 100%;
+}
+
+.table :deep(.arco-table-td),
+.table :deep(.arco-table-th) {
+    overflow: hidden;
+}
+
+.user-cell {
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    max-width: 100%;
+    min-width: 0;
+    vertical-align: middle;
+}
+
+.user-cell__avatar {
+    flex-shrink: 0;
+}
+
+.user-cell__name {
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.bind-cell {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    max-width: 100%;
+    min-width: 0;
+    vertical-align: middle;
+}
+
+.bind-cell__avatar {
+    flex-shrink: 0;
+}
+
+.bind-cell__text {
+    min-width: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
 .muted {
     color: var(--color-text-3);
 }

+ 26 - 13
src/pages/admin/workOrder/orderDetail.vue

@@ -2,7 +2,18 @@
     <div class="store-page">
         <Breadcrumb />
         <a-card title="工单详情" :loading="loading">
-            <a-descriptions :data="info" :column="2" />
+            <a-descriptions :column="{ xs: 1, sm: 2 }" bordered size="medium" class="order-info-desc">
+                <a-descriptions-item label="工单标题">{{ data.title || '-' }}</a-descriptions-item>
+                <a-descriptions-item label="工单ID">{{ data.id || '-' }}</a-descriptions-item>
+                <a-descriptions-item label="发起人">
+                    {{ data.userInfo?.[data.create_user]?.username || '-' }}
+                </a-descriptions-item>
+                <a-descriptions-item label="发起人UUID">{{ data.create_user || '-' }}</a-descriptions-item>
+                <a-descriptions-item label="通知邮箱">{{ data.email || '-' }}</a-descriptions-item>
+                <a-descriptions-item label="当前状态">{{ getState(data.state) }}</a-descriptions-item>
+                <a-descriptions-item label="创建时间">{{ data.create_time ? stramptoTime(data.create_time) : '-' }}</a-descriptions-item>
+                <a-descriptions-item label="最后更新时间">{{ data.update_time ? stramptoTime(data.update_time) : '-' }}</a-descriptions-item>
+            </a-descriptions>
             <div class="buttonGroup">
                 <a-button type="primary" size="large" :loading="buttonLoading" @click="CloseOrder()"
                     :disabled="data.state === 2">{{ data.state === 2 ? '已关闭' :
@@ -101,7 +112,6 @@ const loading = ref(false)
 const formLoading = ref(false)
 const buttonLoading = ref(false)
 const data = ref({})
-const info = ref([])
 
 const form = reactive({
     content: '',
@@ -203,17 +213,6 @@ const getOrderDetail = async () => {
             })
 
         data.value = res.data
-
-        info.value = [
-            { label: '工单标题', value: res.data.title },
-            { label: '工单ID', value: res.data.id },
-            { label: '发起人', value: res.data.userInfo[res.data.create_user]?.username },
-            { label: '发起人UUID', value: res.data.create_user },
-            { label: '通知邮箱', value: res.data.email },
-            { label: '创建时间', value: stramptoTime(res.data.create_time) },
-            { label: '最后更新时间', value: stramptoTime(res.data.update_time) },
-            { label: '当前状态', value: getState(res.data.state) }
-        ]
     } catch (error) {
         Notification.error({
             title: '获取路径数据失败!',
@@ -337,6 +336,20 @@ const stramptoTime = (time) => {
     }
 }
 
+.order-info-desc {
+    :deep(.arco-descriptions-item-label) {
+        white-space: nowrap;
+    }
+
+    :deep(.arco-descriptions-item-value) {
+        word-break: break-all;
+    }
+}
+
+.buttonGroup {
+    margin-top: 16px;
+}
+
 .reply-form {
     max-width: 640px;
     width: 100%;

+ 7 - 1
src/pages/admin/workOrder/orderList.vue

@@ -3,6 +3,7 @@
         <Breadcrumb />
 
         <a-card title="工单列表">
+            <AppQueryFilter>
             <a-row class="query-row">
                 <a-col :flex="'1000px'" class="query-main">
                     <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -42,6 +43,7 @@
                     </a-space>
                 </a-col>
             </a-row>
+            </AppQueryFilter>
 
 
             <a-table :bordered="false" :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
@@ -105,6 +107,7 @@
 import { ref, reactive, onMounted } from 'vue'
 import { adminOrderList } from '@/api/workOrder'
 import { Notification } from '@arco-design/web-vue'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 
 const pagination = reactive({
     total: 0,
@@ -132,7 +135,7 @@ const reset = () => {
     getOrderList()
 }
 
-const columns = [
+const columnDefs = [
     {
         title: '工单ID',
         dataIndex: 'id',
@@ -175,6 +178,9 @@ const columns = [
     }
 ]
 
+const { useTableColumns } = useResponsiveTable()
+const columns = useTableColumns(columnDefs)
+
 const loading = ref(false)
 const data = ref([])
 

+ 13 - 3
src/pages/lepao/accountList/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div class="store-page">
     <Breadcrumb />
 
@@ -75,6 +75,7 @@
         </a-dropdown>
       </div>
 
+      <AppQueryFilter>
       <a-row class="queryForm app-query-form query-row">
         <a-col :flex="'1000px'" class="query-main">
           <a-form :model="queryDataForm" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -153,6 +154,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
 
@@ -298,7 +300,7 @@
             </template>
           </a-table-column>
 
-          <a-table-column title="" fixed="right" :width="100">
+          <a-table-column title="" :fixed="tableFixed('right')" :width="100">
             <template #cell="{ record }">
               <a-dropdown :popup-max-height="false" trigger="hover" @click.stop>
                 <a-button>操作 <icon-down /></a-button>
@@ -403,6 +405,9 @@ import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.v
 import { useRoute } from 'vue-router'
 import { getNotice, getSemesterTimestamps } from '@/utils/util'
 import { hasPermission } from '@/utils/permission'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+
+const { tableFixed } = useResponsiveTable()
 
 const notice = ref('')
 
@@ -892,11 +897,16 @@ onUnmounted(() => {
 </script>
 
 <style scoped lang="less">
+@import '@/styles/store-theme.less';
+
 .queryForm {
-  margin-top: 20px;
   padding: 0;
   width: 100%;
   box-sizing: border-box;
+
+  @media (min-width: (@app-breakpoint-md + 1px)) {
+    margin-top: 20px;
+  }
 }
 
 .buttonGroup {

+ 7 - 1
src/pages/lepao/countLedger/index.vue

@@ -5,6 +5,7 @@
     <userCard />
 
     <a-card title="乐跑次数明细" style="margin-top: 15px;">
+      <AppQueryFilter>
       <a-row class="queryForm app-query-form">
         <a-col :flex="'1000px'">
           <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
@@ -44,6 +45,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
 
@@ -216,8 +218,12 @@ onMounted(() => {
 </script>
 
 <style scoped lang="less">
+@import '@/styles/store-theme.less';
+
 .queryForm {
-  margin-bottom: 16px;
+  @media (min-width: (@app-breakpoint-md + 1px)) {
+    margin-bottom: 16px;
+  }
 }
 
 .summary-muted {

+ 6 - 1
src/pages/lepao/lepaoRecords/index.vue

@@ -3,6 +3,7 @@
   <div class="store-page">
     <Breadcrumb />
     <a-card title="乐跑记录">
+      <AppQueryFilter>
       <a-row class="query-row">
         <a-col :flex="'1000px'" class="query-main">
           <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
@@ -51,6 +52,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
 
@@ -142,7 +144,7 @@
               {{ stramptoTime(record.time) }}
             </template>
           </a-table-column>
-          <a-table-column title="" :width="100" fixed="right">
+          <a-table-column title="" :width="100" :fixed="tableFixed('right')">
             <template #cell="{ record }">
               <a-button @click="openDetail(record)">查看详情</a-button>
             </template>
@@ -163,6 +165,9 @@ import { IconSearch } from '@arco-design/web-vue/es/icon'
 import { useUserStore } from '@/store/modules/user'
 import { useRoute } from 'vue-router'
 import { getNotice, getSemesterTimestamps } from '@/utils/util'
+import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+
+const { tableFixed } = useResponsiveTable()
 
 const notice = ref('')
 const router = useRouter()

+ 3 - 1
src/pages/path/pathList.vue

@@ -3,7 +3,8 @@
         <Breadcrumb />
 
         <a-card title="路径列表">
-            <a-row>
+            <AppQueryFilter>
+            <a-row class="query-row app-query-form">
                 <a-col :flex="1">
                     <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
                         label-align="left">
@@ -63,6 +64,7 @@
                     </a-space>
                 </a-col>
             </a-row>
+            </AppQueryFilter>
 
             <a-table :bordered="false" :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
                 :pagination="{

+ 16 - 33
src/pages/power/accountList.vue

@@ -145,6 +145,7 @@
       hoverable
       :loading="listLoading"
       class="change-table"
+      :scroll="{ x: 720 }"
       :pagination="{
         showPageSize: true,
         showJumper: true,
@@ -156,6 +157,9 @@
       @page-change="handlePageChange"
       @page-size-change="handlePageSizeChange"
     >
+      <template #time="{ record }">
+        {{ formatTime(record.time) }}
+      </template>
       <template #balance="{ record }">
         ¥{{ record.balance }}
       </template>
@@ -167,9 +171,6 @@
           ¥{{ (Number(record.balance) - Number(record.old_balance)).toFixed(2) }}
         </a-tag>
       </template>
-      <template #time="{ record }">
-        {{ formatTime(record.time) }}
-      </template>
     </a-table>
   </a-modal>
 </template>
@@ -214,6 +215,15 @@ const handlePageSizeChange = (size) => {
 
 const changeList = ref([])
 const listLoading = ref(false)
+
+const listColumns = [
+  { title: '记录时间', slotName: 'time', width: 170, ellipsis: true, tooltip: true },
+  { title: '扣费时间', dataIndex: 'change_time', width: 170, ellipsis: true, tooltip: true },
+  { title: '电费余额', slotName: 'balance', width: 120, ellipsis: true, tooltip: true },
+  { title: '原余额', slotName: 'old_balance', width: 120, ellipsis: true, tooltip: true },
+  { title: '变动金额', slotName: 'change', width: 120, ellipsis: true, tooltip: true }
+]
+
 const GetChangeRecord = async (id) => {
   try {
     listLoading.value = true
@@ -227,8 +237,9 @@ const GetChangeRecord = async (id) => {
         content: res?.msg ?? '请稍后再试'
       })
     }
-    changeList.value = res.data
-    pagination.total = res.data.length
+    const rows = Array.isArray(res.data) ? res.data : res.data?.list ?? []
+    changeList.value = rows
+    pagination.total = res.pagination?.total ?? rows.length
   } catch (error) {
     Notification.error({
       title: '获取电费变更记录失败',
@@ -251,34 +262,6 @@ const form = reactive({
   lowest: 10.0
 })
 
-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([])
 const buildings = ref([])

+ 1 - 1
src/pages/qxs/getBookList.vue

@@ -4,7 +4,7 @@
 
         <a-card title="趣选书 · 书单查询" class="card">
             <div class="userLogin">
-                <a-form :model="form" direction="vertical" size="large" class="login-form">
+                <a-form :model="form" size="large" class="login-form">
                     <a-form-item field="username" label="用户名">
                         <a-input v-model="form.username" :style="{ width: '320px' }" placeholder="请输入趣选书账户" allow-clear>
                             <template #prefix>

+ 24 - 10
src/pages/store/goodsDetail/index.vue

@@ -472,7 +472,6 @@ getGoodsDetail()
 }
 
 .submit-btn {
-  margin-top: 8px;
   background: @store-primary !important;
   border-color: @store-primary !important;
 }
@@ -486,19 +485,34 @@ getGoodsDetail()
     width: 100%;
   }
 
-  :deep(.checkout-drawer .arco-drawer) {
+  :deep(.checkout-drawer.arco-drawer) {
     width: 100vw !important;
     max-width: 100vw !important;
   }
+}
+</style>
 
-  :deep(.checkout-drawer .arco-drawer-footer) {
-    display: grid;
-    grid-template-columns: 1fr;
-    gap: 10px;
-  }
+<style lang="less">
+@import '@/styles/store-theme.less';
 
-  :deep(.checkout-drawer .arco-drawer-footer .arco-btn) {
-    width: 100%;
-  }
+/* drawer 挂载到 body,使用非 scoped 保证页脚按钮全宽居中 */
+.checkout-drawer .arco-drawer-footer {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  justify-content: center;
+  gap: 10px;
+  padding: 12px 20px 16px;
+  box-sizing: border-box;
+}
+
+.checkout-drawer .arco-drawer-footer .arco-btn {
+  width: 100%;
+  margin: 0;
+}
+
+.checkout-drawer .arco-drawer-footer .submit-btn {
+  background: @store-primary !important;
+  border-color: @store-primary !important;
 }
 </style>

+ 65 - 5
src/pages/store/orders/orderDetail/index.vue

@@ -14,9 +14,19 @@
               <div class="pay-banner__desc">{{ paymentCountdownText }}</div>
             </div>
           </div>
-          <a-button type="primary" size="large" class="pay-banner__btn" @click="pay">
-            立即支付 ¥{{ data?.price }}
-          </a-button>
+          <div class="pay-banner__actions">
+            <a-button type="primary" size="large" class="pay-banner__btn" @click="pay">
+              立即支付 ¥{{ data?.price }}
+            </a-button>
+            <a-button
+              size="large"
+              class="pay-banner__btn"
+              :loading="cancelLoading"
+              @click="handleCancelOrder"
+            >
+              取消订单
+            </a-button>
+          </div>
         </div>
 
         <a-card :bordered="false" class="status-card">
@@ -67,6 +77,14 @@
         </a-card>
 
         <div class="detail-actions">
+          <a-button
+            v-if="data?.state === 0"
+            status="danger"
+            :loading="cancelLoading"
+            @click="handleCancelOrder"
+          >
+            取消订单
+          </a-button>
           <a-button @click="$router.push('/store/myOrder')">返回订单列表</a-button>
           <a-button type="outline" @click="$router.push('/store/goodsList')">继续购物</a-button>
         </div>
@@ -78,9 +96,9 @@
 
 <script setup>
 import { ref, computed, onUnmounted, onMounted } from 'vue'
-import { orderDeatil } from '@/api/order'
+import { orderDeatil, cancelOrder } from '@/api/order'
 import { useRoute } from 'vue-router'
-import { Notification, Message } from '@arco-design/web-vue'
+import { Notification, Message, Modal } from '@arco-design/web-vue'
 import OrderStateTag from '@/components/store/OrderStateTag.vue'
 import OrderProgressSteps from '@/components/store/OrderProgressSteps.vue'
 import {
@@ -94,6 +112,7 @@ const route = useRoute()
 const { id } = route.params
 
 const loading = ref(true)
+const cancelLoading = ref(false)
 const data = ref({})
 const payData = ref({})
 const content = ref('')
@@ -255,6 +274,40 @@ const pay = () => {
   openPaymentWindow(payData.value.payUrl, payData.value.payData)
 }
 
+const handleCancelOrder = () => {
+  Modal.confirm({
+    title: '取消订单',
+    content: '确定要取消该订单吗?取消后需重新下单购买。',
+    okText: '确认取消',
+    cancelText: '再想想',
+    okButtonProps: { status: 'danger' },
+    onOk: async () => {
+      try {
+        cancelLoading.value = true
+        const res = await cancelOrder({ orderId: id })
+        if (!res || res.code !== 0) {
+          Notification.error({
+            title: '取消订单失败',
+            content: res?.msg ?? '请稍后再试'
+          })
+          return
+        }
+        Message.success(res.msg || '订单已取消')
+        payData.value = {}
+        stopPendingOrderTimers()
+        await getOrderDetail()
+      } catch (error) {
+        Notification.error({
+          title: '取消订单失败',
+          content: error.message || '请稍后再试'
+        })
+      } finally {
+        cancelLoading.value = false
+      }
+    }
+  })
+}
+
 function openPaymentWindow(payUrl, formData) {
   const form = document.createElement('form')
   form.method = 'POST'
@@ -319,6 +372,13 @@ function openPaymentWindow(payUrl, formData) {
     margin-top: 4px;
   }
 
+  &__actions {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    flex-shrink: 0;
+  }
+
   &__btn {
     border-radius: 999px;
     flex-shrink: 0;

+ 53 - 10
src/pages/store/orders/orderList/index.vue

@@ -50,14 +50,23 @@
               <time class="time">{{ formatStoreTime(record.create_time) }}</time>
             </div>
             <div class="order-card__actions">
-              <a-button
-                v-if="record.state === 0"
-                type="primary"
-                size="small"
-                @click.stop="goDetail(record.orderId)"
-              >
-                去支付
-              </a-button>
+              <template v-if="record.state === 0">
+                <a-button
+                  type="primary"
+                  size="small"
+                  @click.stop="goDetail(record.orderId)"
+                >
+                  去支付
+                </a-button>
+                <a-button
+                  size="small"
+                  status="danger"
+                  :loading="cancelingOrderId === record.orderId"
+                  @click.stop="handleCancelOrder(record)"
+                >
+                  取消订单
+                </a-button>
+              </template>
               <a-button v-else type="text" size="small" @click.stop="goDetail(record.orderId)">
                 查看详情
               </a-button>
@@ -72,8 +81,8 @@
 <script setup>
 import { ref, computed } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
-import { getMyOrder } from '@/api/order'
-import { Notification } from '@arco-design/web-vue'
+import { getMyOrder, cancelOrder } from '@/api/order'
+import { Notification, Message, Modal } from '@arco-design/web-vue'
 import OrderStateTag from '@/components/store/OrderStateTag.vue'
 import { getNotice } from '@/utils/util'
 import { formatStoreTime, getPayTypeLabel } from '@/utils/storeFormat'
@@ -82,6 +91,7 @@ const router = useRouter()
 const notice = ref('')
 const data = ref([])
 const loading = ref(false)
+const cancelingOrderId = ref('')
 const statusTab = ref('all')
 
 const statusTabs = [
@@ -100,6 +110,37 @@ const displayList = computed(() => {
 
 const goDetail = (orderId) => router.push(`/store/orderDetail/${orderId}`)
 
+const handleCancelOrder = (record) => {
+  Modal.confirm({
+    title: '取消订单',
+    content: `确定要取消订单 ${record.orderId} 吗?`,
+    okText: '确认取消',
+    cancelText: '再想想',
+    okButtonProps: { status: 'danger' },
+    onOk: async () => {
+      try {
+        cancelingOrderId.value = record.orderId
+        const res = await cancelOrder({ orderId: record.orderId })
+        if (!res || res.code !== 0) {
+          return Notification.error({
+            title: '取消订单失败',
+            content: res?.msg ?? '请稍后再试'
+          })
+        }
+        Message.success(res.msg || '订单已取消')
+        await GetMyOrder()
+      } catch (error) {
+        Notification.error({
+          title: '取消订单失败',
+          content: error.message || '请稍后再试'
+        })
+      } finally {
+        cancelingOrderId.value = ''
+      }
+    }
+  })
+}
+
 const GetMyOrder = async () => {
   try {
     loading.value = true
@@ -235,6 +276,8 @@ GetNotice()
     margin-top: 12px;
     display: flex;
     justify-content: flex-end;
+    gap: 8px;
+    flex-wrap: wrap;
   }
 }
 </style>

+ 2 - 0
src/pages/store/sendCountRecords/index.vue

@@ -3,6 +3,7 @@
     <Breadcrumb />
 
     <a-card title="赠送记录">
+      <AppQueryFilter>
       <a-row class="query-row">
         <a-col :flex="'1000px'" class="query-main">
           <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }"
@@ -39,6 +40,7 @@
           </a-space>
         </a-col>
       </a-row>
+      </AppQueryFilter>
 
       <a-table :bordered="false" :data="data" stripe hoverable column-resizable class="table" :loading="loading" :columns="columns"
         :pagination="{

+ 7 - 0
src/style.css

@@ -205,5 +205,12 @@ h1 {
 
   .store-page .arco-card .arco-form .arco-form-item-label-col > label {
     white-space: nowrap;
+    justify-content: flex-start !important;
+    text-align: left !important;
+  }
+
+  .store-page .arco-card .arco-form .arco-form-item-label-col {
+    justify-content: flex-start !important;
+    text-align: left !important;
   }
 }

+ 67 - 0
src/styles/store-theme.less

@@ -194,6 +194,73 @@
   }
 }
 
+// —— 移动端筛选折叠(桌面端始终展示表单项)——
+.app-query-filter {
+  margin-bottom: 16px;
+
+  &__chevron {
+    margin-left: 4px;
+    transition: transform 0.2s ease;
+  }
+
+  @media (max-width: @app-breakpoint-md) {
+    margin-bottom: 12px;
+
+    &__toggle {
+      width: 100%;
+      align-items: center;
+      justify-content: center;
+      gap: 6px;
+      border-radius: @store-radius-sm;
+      color: @store-primary;
+      border-color: #c5d9cc;
+      background: @store-card-bg;
+    }
+
+    &.is-expanded {
+      .app-query-filter__panel {
+        margin-top: 10px;
+      }
+
+      .app-query-filter__chevron {
+        transform: rotate(180deg);
+      }
+    }
+
+    // 避免各页 queryForm / query-row 额外 margin 与组件间距叠加
+    .queryForm,
+    .query-row {
+      margin-top: 0 !important;
+      margin-bottom: 0 !important;
+    }
+  }
+
+  @media (min-width: (@app-breakpoint-md + 1px)) {
+    &__toggle {
+      display: none !important;
+    }
+  }
+}
+
+@media (max-width: @app-breakpoint-md) {
+  .app-query-filter + .arco-alert,
+  .app-query-filter + .notice {
+    margin-top: 0 !important;
+  }
+
+  // 水平表单在移动端堆叠为上下行时,label 默认仍右对齐,统一改为靠左
+  .arco-form-item-label-col {
+    justify-content: flex-start !important;
+    text-align: left !important;
+  }
+
+  .arco-form-item-label-col > .arco-form-item-label,
+  .arco-form-item-label-col > label {
+    justify-content: flex-start !important;
+    text-align: left !important;
+  }
+}
+
 @media (max-width: @app-breakpoint-lg) {
   .app-query-form .arco-row > .arco-col,
   .queryForm .arco-row > .arco-col {