OrderManagement.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <template>
  2. <div class="page-container">
  3. <div class="page-header">
  4. <div>
  5. <h1 class="page-title">订单管理</h1>
  6. <p class="page-subtitle">查看、搜索和管理所有发货订单</p>
  7. </div>
  8. <div class="page-actions">
  9. <el-button @click="$emit('exportExcel')">
  10. <el-icon style="margin-right: 4px"><Download /></el-icon>导出 Excel
  11. </el-button>
  12. <el-button type="primary" @click="$emit('openImport')">
  13. <el-icon style="margin-right: 4px"><Upload /></el-icon>导入物流
  14. </el-button>
  15. </div>
  16. </div>
  17. <!-- 统计卡片 -->
  18. <div class="stats-grid">
  19. <div class="stat-card" v-for="s in statCards" :key="s.label">
  20. <div class="stat-card-header">
  21. <span class="stat-card-label">{{ s.label }}</span>
  22. <div class="stat-card-icon" :class="s.color">
  23. <component :is="s.icon" />
  24. </div>
  25. </div>
  26. <div class="stat-card-value">{{ s.value }}</div>
  27. </div>
  28. </div>
  29. <!-- 筛选区 -->
  30. <div class="filter-bar">
  31. <div class="filter-row">
  32. <div class="filter-field">
  33. <label>订单状态</label>
  34. <el-select v-model="form.status" placeholder="全部" clearable style="width: 130px">
  35. <el-option label="待处理" :value="0" />
  36. <el-option label="已完成" :value="1" />
  37. <el-option label="已取消" :value="2" />
  38. <el-option label="已发货" :value="3" />
  39. </el-select>
  40. </div>
  41. <div class="filter-field">
  42. <label>下单日期</label>
  43. <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" style="width: 240px" />
  44. </div>
  45. <div class="filter-field">
  46. <label>收货人</label>
  47. <el-input v-model="form.receiver_name" placeholder="输入收货人" clearable />
  48. </div>
  49. <div class="filter-field">
  50. <label>手机号</label>
  51. <el-input v-model="form.receiver_phone" placeholder="输入手机号" clearable />
  52. </div>
  53. <div class="filter-field">
  54. <label>订单号</label>
  55. <el-input v-model="form.order_no" placeholder="输入订单号" clearable />
  56. </div>
  57. <div class="filter-actions">
  58. <el-button @click="handleReset(); $emit('query')">重置</el-button>
  59. <el-button type="primary" @click="$emit('query')">查询</el-button>
  60. </div>
  61. </div>
  62. </div>
  63. <!-- 数据表格 -->
  64. <div class="table-container">
  65. <div class="table-toolbar">
  66. <div class="table-toolbar-left">
  67. <span class="table-title">订单列表</span>
  68. <span class="count-badge">共 {{ total }} 条</span>
  69. </div>
  70. </div>
  71. <el-table :data="tableData" v-loading="loading" style="width: 100%" :header-cell-style="{ background: 'var(--bg-subtle)', color: 'var(--text-tertiary)', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }" :row-style="{ height: '48px' }" @row-mouseover="true">
  72. <el-table-column prop="批次号" label="批次号" width="160">
  73. <template #default="{ row }"><span class="mono">{{ row['批次号'] }}</span></template>
  74. </el-table-column>
  75. <el-table-column prop="订单号" label="订单号" width="150">
  76. <template #default="{ row }"><span class="mono">{{ row['订单号'] }}</span></template>
  77. </el-table-column>
  78. <el-table-column prop="物流公司" label="物流公司" width="120" />
  79. <el-table-column prop="物流单号" label="物流单号" width="160">
  80. <template #default="{ row }">
  81. <span class="mono">{{ row['物流单号'] || '—' }}</span>
  82. </template>
  83. </el-table-column>
  84. <el-table-column prop="订单状态" label="订单状态" width="100" align="center">
  85. <template #default="{ row }">
  86. <span class="status-badge" :class="getStatusClass(row['订单状态'])">
  87. <span class="dot"></span>{{ row['订单状态'] }}
  88. </span>
  89. </template>
  90. </el-table-column>
  91. <el-table-column prop="下单时间" label="下单时间" width="180">
  92. <template #default="{ row }"><span style="color: var(--text-secondary); font-size: var(--text-sm);">{{ row['下单时间'] }}</span></template>
  93. </el-table-column>
  94. <el-table-column prop="卡号" label="卡号" width="120">
  95. <template #default="{ row }"><span class="mono">{{ row['卡号'] }}</span></template>
  96. </el-table-column>
  97. <el-table-column prop="员工姓名" label="员工姓名" width="100" />
  98. <el-table-column prop="收货人" label="收货人" width="100" />
  99. <el-table-column prop="手机号" label="手机号" width="130" />
  100. <el-table-column prop="收货地址" label="收货地址" width="250" show-overflow-tooltip />
  101. <el-table-column prop="商品名称" label="商品名称" width="150" />
  102. <el-table-column prop="数量" label="数量" width="70" align="center" />
  103. <el-table-column prop="单价" label="单价" width="90" align="right" />
  104. <el-table-column prop="小计" label="小计" width="90" align="right" />
  105. <el-table-column prop="备注" label="备注" width="150" show-overflow-tooltip />
  106. </el-table>
  107. <div class="table-footer">
  108. <div class="table-footer-info">显示第 {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, total) }} 条</div>
  109. <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]" :total="total" layout="prev, pager, next, sizes" @size-change="$emit('query')" @current-change="$emit('query')" />
  110. </div>
  111. </div>
  112. </div>
  113. </template>
  114. <script setup>
  115. import { reactive, ref, computed } from 'vue'
  116. import { Download, Upload } from '@element-plus/icons-vue'
  117. const props = defineProps({
  118. tableData: Array,
  119. total: Number,
  120. loading: Boolean,
  121. stats: Object
  122. })
  123. defineEmits(['query', 'exportExcel', 'openImport'])
  124. const form = reactive({ receiver_name: '', receiver_phone: '', order_no: '', status: 0 })
  125. const dateRange = ref(null)
  126. const currentPage = ref(1)
  127. const pageSize = ref(20)
  128. const statCards = computed(() => [
  129. { label: '总订单数', value: props.stats?.total ?? '—', color: 'primary', icon: 'Box' },
  130. { label: '待处理', value: props.stats?.pending ?? '—', color: 'warning', icon: 'Clock' },
  131. { label: '已发货', value: props.stats?.shipped ?? '—', color: 'success', icon: 'CircleCheck' },
  132. { label: '已取消', value: props.stats?.cancelled ?? '—', color: 'info', icon: 'CircleClose' },
  133. ])
  134. function getParams() {
  135. const params = { ...form }
  136. if (dateRange.value?.length === 2) {
  137. params.start_date = dateRange.value[0]
  138. params.end_date = dateRange.value[1]
  139. }
  140. if (params.status === null || params.status === '') delete params.status
  141. return params
  142. }
  143. function handleReset() {
  144. form.receiver_name = ''; form.receiver_phone = ''; form.order_no = ''; form.status = 0
  145. dateRange.value = null
  146. }
  147. function getStatusClass(status) {
  148. const map = { '待处理': 'warning', '已完成': 'success', '已取消': 'danger', '已发货': 'info' }
  149. return map[status] || 'neutral'
  150. }
  151. defineExpose({ getParams, handleReset, currentPage, pageSize })
  152. </script>
  153. <style scoped>
  154. .page-container { animation: slideUp 0.4s var(--ease-out); }
  155. .page-header {
  156. display: flex; align-items: flex-start; justify-content: space-between;
  157. gap: var(--space-4); margin-bottom: var(--space-6);
  158. }
  159. .page-title { font-size: var(--text-xl); font-weight: 800; letter-spacing: -0.01em; color: var(--text-primary); }
  160. .page-subtitle { font-size: var(--text-sm); color: var(--text-secondary); margin-top: 2px; }
  161. .page-actions { display: flex; gap: var(--space-2); flex-shrink: 0; }
  162. .stats-grid {
  163. display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4);
  164. margin-bottom: var(--space-6);
  165. }
  166. .stat-card {
  167. background: var(--bg-surface); border: 1px solid var(--border-subtle);
  168. border-radius: var(--radius-lg); padding: var(--space-5);
  169. box-shadow: var(--shadow-xs); transition: box-shadow var(--dur-normal) var(--ease-out);
  170. }
  171. .stat-card:hover { box-shadow: var(--shadow-sm); }
  172. .stat-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-3); }
  173. .stat-card-label { font-size: var(--text-sm); color: var(--text-secondary); font-weight: 500; }
  174. .stat-card-icon {
  175. width: 36px; height: 36px; border-radius: var(--radius-md);
  176. display: flex; align-items: center; justify-content: center; font-size: 18px;
  177. }
  178. .stat-card-icon.primary { background: var(--primary-50); color: var(--primary-600); }
  179. .stat-card-icon.success { background: var(--success-50); color: var(--success-600); }
  180. .stat-card-icon.warning { background: var(--warning-50); color: var(--warning-600); }
  181. .stat-card-icon.info { background: var(--info-50); color: var(--info-600); }
  182. .stat-card-value { font-size: var(--text-2xl); font-weight: 800; letter-spacing: -0.02em; color: var(--text-primary); }
  183. .filter-bar {
  184. background: var(--bg-surface); border: 1px solid var(--border-subtle);
  185. border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5);
  186. margin-bottom: var(--space-4); box-shadow: var(--shadow-xs);
  187. }
  188. .filter-row { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; }
  189. .filter-field { display: flex; flex-direction: column; gap: var(--space-1); }
  190. .filter-field label { font-size: var(--text-xs); font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.04em; }
  191. .filter-field .el-input, .filter-field .el-select { min-width: 130px; }
  192. .filter-actions { display: flex; gap: var(--space-2); margin-left: auto; }
  193. .table-container {
  194. background: var(--bg-surface); border: 1px solid var(--border-subtle);
  195. border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-xs);
  196. }
  197. .table-toolbar {
  198. display: flex; align-items: center; justify-content: space-between;
  199. padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-subtle);
  200. }
  201. .table-toolbar-left { display: flex; align-items: center; gap: var(--space-3); }
  202. .table-title { font-size: var(--text-md); font-weight: 700; color: var(--text-primary); }
  203. .count-badge {
  204. font-size: var(--text-xs); font-weight: 600; padding: 2px 8px;
  205. border-radius: var(--radius-full); background: var(--gray-100); color: var(--gray-600);
  206. }
  207. .table-footer {
  208. display: flex; align-items: center; justify-content: space-between;
  209. padding: var(--space-4) var(--space-5); border-top: 1px solid var(--border-subtle);
  210. }
  211. .table-footer-info { font-size: var(--text-sm); color: var(--text-secondary); }
  212. @keyframes slideUp { from { opacity: 0; transform: translateY(12px); } }
  213. </style>