index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <!-- 叁强鲜配工作台页面 -->
  2. <template>
  3. <div class="console-container">
  4. <div class="page-head">
  5. <h1>运营工作台</h1>
  6. <p class="page-desc">统一查看订单、配送、结算与待办事项,优先处理时效预警任务。</p>
  7. </div>
  8. <!-- 预警提示 -->
  9. <div v-if="warningCount > 0" class="alert-bar" @click="goToWarning">
  10. <el-icon class="alert-icon"><WarningFilled /></el-icon>
  11. <span class="alert-text">您有 <strong>{{ warningCount }}</strong> 个配送单即将超时,请及时处理!</span>
  12. <span class="alert-btn">立即处理</span>
  13. </div>
  14. <!-- 数据统计卡片 -->
  15. <el-row :gutter="16" class="stats-row">
  16. <el-col :xs="12" :sm="8" :md="5" v-for="stat in statsData" :key="stat.key">
  17. <div class="stats-card" @click="handleStatClick(stat.key)">
  18. <div class="stats-icon" :class="stat.colorClass">
  19. <el-icon :size="24"><component :is="stat.icon" /></el-icon>
  20. </div>
  21. <div class="stats-info">
  22. <div class="stats-num">{{ stat.value }}</div>
  23. <div class="stats-label">{{ stat.label }}</div>
  24. </div>
  25. </div>
  26. </el-col>
  27. </el-row>
  28. <!-- 待办事项 -->
  29. <el-row :gutter="16" class="todo-row">
  30. <el-col :xs="24" :sm="12" :md="6" v-for="todo in todoList" :key="todo.type">
  31. <div class="todo-card" :class="todo.className" @click="handleTodoClick(todo.path)">
  32. <div class="todo-header">
  33. <span class="todo-title">{{ todo.title }}</span>
  34. <span class="todo-count" :class="todo.countClass">{{ todo.count }}</span>
  35. </div>
  36. <div class="todo-list">
  37. <div v-for="(item, index) in todo.items.slice(0, 2)" :key="index" class="todo-item">
  38. <span class="todo-item-text">{{ item }}</span>
  39. <span class="todo-item-action" @click.stop="handleTodoAction(todo.path)">
  40. {{ todo.actionText }}
  41. </span>
  42. </div>
  43. </div>
  44. </div>
  45. </el-col>
  46. </el-row>
  47. <!-- 快捷操作 -->
  48. <el-row :gutter="16" class="quick-row">
  49. <el-col :xs="12" :sm="6" v-for="action in quickActions" :key="action.key">
  50. <div class="quick-btn" @click="handleQuickAction(action.path)">
  51. <div class="quick-icon" :class="action.colorClass">
  52. <el-icon :size="20"><component :is="action.icon" /></el-icon>
  53. </div>
  54. <span class="quick-text">{{ action.text }}</span>
  55. <el-icon class="quick-arrow"><ArrowRight /></el-icon>
  56. </div>
  57. </el-col>
  58. </el-row>
  59. <!-- 最新订单 -->
  60. <section class="panel orders-card">
  61. <div class="panel-top">
  62. <h2>最新订单</h2>
  63. <el-button type="primary" link @click="goToOrderList">查看更多</el-button>
  64. </div>
  65. <div class="list-body list-wrap">
  66. <el-table class="list-table" :data="orderList" v-loading="loading" stripe>
  67. <el-table-column prop="orderNo" label="订单编号" min-width="180" />
  68. <el-table-column prop="clientName" label="客户名称" min-width="150" />
  69. <el-table-column prop="region" label="区域" min-width="100" />
  70. <el-table-column prop="totalAmount" label="订单金额(元)" min-width="120">
  71. <template #default="{ row }">
  72. <span class="amount-text">¥{{ row.totalAmount }}</span>
  73. </template>
  74. </el-table-column>
  75. <el-table-column prop="status" label="订单状态" min-width="130">
  76. <template #default="{ row }">
  77. <el-tag :type="getStatusType(row.status)" size="small">
  78. {{ getStatusText(row.status) }}
  79. </el-tag>
  80. </template>
  81. </el-table-column>
  82. <el-table-column prop="createTime" label="下单时间" min-width="180" />
  83. <el-table-column label="操作" min-width="100">
  84. <template #default="{ row }">
  85. <el-button type="primary" link @click="goToOrderDetail(row.id)">查看</el-button>
  86. </template>
  87. </el-table-column>
  88. </el-table>
  89. </div>
  90. </section>
  91. </div>
  92. </template>
  93. <script setup lang="ts">
  94. import { ref, onMounted } from 'vue'
  95. import { useRouter } from 'vue-router'
  96. import {
  97. WarningFilled,
  98. UserFilled,
  99. User,
  100. Shop,
  101. ShoppingCart,
  102. Van,
  103. Money,
  104. Plus,
  105. Wallet,
  106. ArrowRight
  107. } from '@element-plus/icons-vue'
  108. import { ElMessage } from 'element-plus'
  109. defineOptions({ name: 'Console' })
  110. const router = useRouter()
  111. const loading = ref(false)
  112. const warningCount = ref(3)
  113. // 统计数据
  114. const statsData = ref([
  115. { key: 'client', label: '客户总数', value: '1,286', icon: UserFilled, colorClass: 'blue', path: '/admin/user' },
  116. { key: 'supplier', label: '供应商数量', value: '86', icon: Shop, colorClass: 'green', path: '/admin/user' },
  117. { key: 'todayOrder', label: '今日订单', value: '328', icon: ShoppingCart, colorClass: 'orange', path: '/admin/order' },
  118. { key: 'pendingDelivery', label: '待配送', value: '45', icon: Van, colorClass: 'red', path: '/admin/delivery' },
  119. { key: 'todayAmount', label: '今日交易额', value: '¥58,620', icon: Money, colorClass: 'purple', path: '/admin/statistics' }
  120. ])
  121. // 待办事项
  122. const todoList = ref([
  123. {
  124. type: 'quotation',
  125. title: '待审核报价',
  126. count: 5,
  127. className: 'warning',
  128. countClass: 'orange',
  129. actionText: '处理',
  130. path: '/inquiry/quotation-list',
  131. items: ['2026年3月第3周询价...', '城东区域供应商报价...']
  132. },
  133. {
  134. type: 'delivery',
  135. title: '待分配配送',
  136. count: 12,
  137. className: 'danger',
  138. countClass: '',
  139. actionText: '分配',
  140. path: '/admin/delivery',
  141. items: ['ORD202603190003', 'ORD202603190004']
  142. },
  143. {
  144. type: 'cancel',
  145. title: '取消待审核',
  146. count: 3,
  147. className: 'primary',
  148. countClass: 'blue',
  149. actionText: '审核',
  150. path: '/admin/order',
  151. items: ['客户BB超市申请取消...', '客户CC商行申请取消...']
  152. },
  153. {
  154. type: 'settlement',
  155. title: '待结算确认',
  156. count: 8,
  157. className: 'success',
  158. countClass: 'green',
  159. actionText: '确认',
  160. path: '/admin/settlement',
  161. items: ['AA餐厅线下结算...', 'BB超市线下结算...']
  162. }
  163. ])
  164. // 快捷操作
  165. const quickActions = [
  166. { key: 'inquiry', text: '发布询价', icon: Plus, colorClass: 'blue', path: '/inquiry/inquiry-list' },
  167. { key: 'delivery', text: '分配配送', icon: Van, colorClass: 'orange', path: '/admin/delivery' },
  168. { key: 'settlement', text: '确认结算', icon: Wallet, colorClass: 'green', path: '/admin/settlement' },
  169. { key: 'user', text: '新增用户', icon: User, colorClass: 'red', path: '/admin/user' }
  170. ]
  171. // 订单列表
  172. const orderList = ref([
  173. {
  174. id: '1',
  175. orderNo: 'ORD202603190001',
  176. clientName: 'XX餐厅',
  177. region: '城东',
  178. totalAmount: '1,286.00',
  179. status: 'pending_payment',
  180. createTime: '2026-03-19 10:23:45'
  181. },
  182. {
  183. id: '2',
  184. orderNo: 'ORD202603190002',
  185. clientName: 'YY超市',
  186. region: '城西',
  187. totalAmount: '2,560.00',
  188. status: 'pending_delivery',
  189. createTime: '2026-03-19 09:15:22'
  190. },
  191. {
  192. id: '3',
  193. orderNo: 'ORD202603180056',
  194. clientName: 'ZZ食堂',
  195. region: '城南',
  196. totalAmount: '856.00',
  197. status: 'delivering',
  198. createTime: '2026-03-18 16:45:30'
  199. },
  200. {
  201. id: '4',
  202. orderNo: 'ORD202603180055',
  203. clientName: 'AA饭店',
  204. region: '城东',
  205. totalAmount: '3,200.00',
  206. status: 'completed',
  207. createTime: '2026-03-18 14:20:18'
  208. }
  209. ])
  210. // 获取状态类型
  211. const getStatusType = (status: string) => {
  212. const typeMap: Record<string, string> = {
  213. pending_payment: 'warning',
  214. pending_delivery: 'primary',
  215. delivering: 'info',
  216. partial_delivered: 'warning',
  217. pending_confirm: 'primary',
  218. completed: 'success',
  219. cancelled: 'info'
  220. }
  221. return typeMap[status] || 'info'
  222. }
  223. // 获取状态文本
  224. const getStatusText = (status: string) => {
  225. const textMap: Record<string, string> = {
  226. pending_payment: '待支付',
  227. pending_delivery: '待分配配送',
  228. delivering: '配送中',
  229. partial_delivered: '部分送达',
  230. pending_confirm: '待确认收货',
  231. completed: '已完成',
  232. cancelled: '已取消'
  233. }
  234. return textMap[status] || status
  235. }
  236. // 跳转预警页面
  237. const goToWarning = () => {
  238. router.push('/admin/delivery-warning')
  239. }
  240. // 统计卡片点击
  241. const handleStatClick = (key: string) => {
  242. const stat = statsData.value.find((s) => s.key === key)
  243. if (stat?.path) {
  244. router.push(stat.path)
  245. }
  246. }
  247. // 待办点击
  248. const handleTodoClick = (path: string) => {
  249. router.push(path)
  250. }
  251. // 待办操作
  252. const handleTodoAction = (path: string) => {
  253. router.push(path)
  254. }
  255. // 快捷操作
  256. const handleQuickAction = (path: string) => {
  257. router.push(path)
  258. }
  259. // 跳转订单列表
  260. const goToOrderList = () => {
  261. router.push('/admin/order')
  262. }
  263. // 跳转订单详情
  264. const goToOrderDetail = (id: string) => {
  265. router.push(`/admin/order/${id}`)
  266. }
  267. onMounted(() => {
  268. // TODO: 从后端获取数据
  269. })
  270. </script>
  271. <style lang="scss" scoped>
  272. .console-container {
  273. .alert-bar {
  274. display: flex;
  275. align-items: center;
  276. gap: 12px;
  277. padding: 12px 16px;
  278. margin-bottom: 16px;
  279. background: #fffbeb;
  280. border: 1px solid #fde68a;
  281. border-radius: var(--mint-radius);
  282. cursor: pointer;
  283. transition: 0.2s ease;
  284. &:hover {
  285. background: #fef3c7;
  286. }
  287. .alert-icon {
  288. color: #d97706;
  289. font-size: 18px;
  290. }
  291. .alert-text {
  292. flex: 1;
  293. color: #92400e;
  294. font-size: 13px;
  295. }
  296. .alert-btn {
  297. color: var(--mint-primary-dark);
  298. font-size: 13px;
  299. font-weight: 600;
  300. }
  301. }
  302. }
  303. .stats-row,
  304. .todo-row,
  305. .quick-row {
  306. margin-bottom: 16px;
  307. }
  308. .stats-card,
  309. .todo-card,
  310. .quick-btn {
  311. background: var(--mint-surface);
  312. border: 1px solid var(--mint-border);
  313. border-radius: var(--mint-radius);
  314. box-shadow: var(--mint-shadow);
  315. margin-bottom: 0;
  316. }
  317. .stats-card {
  318. min-height: 112px;
  319. padding: 12px 14px;
  320. gap: 12px;
  321. transition: border-color 0.2s ease;
  322. &:hover {
  323. border-color: #b9dbcb;
  324. transform: none;
  325. box-shadow: var(--mint-shadow);
  326. }
  327. .stats-icon {
  328. width: 44px;
  329. height: 44px;
  330. border-radius: 10px;
  331. border: 1px solid rgba(16, 185, 129, 0.12);
  332. background: var(--mint-primary-bg);
  333. color: var(--mint-primary-dark);
  334. &.orange,
  335. &.red {
  336. background: #fffbeb;
  337. border-color: rgba(217, 119, 6, 0.2);
  338. color: #d97706;
  339. }
  340. &.blue,
  341. &.green,
  342. &.purple {
  343. background: var(--mint-primary-bg);
  344. border-color: rgba(16, 185, 129, 0.12);
  345. color: var(--mint-primary-dark);
  346. }
  347. }
  348. .stats-info {
  349. .stats-num {
  350. font-size: 22px;
  351. font-weight: 700;
  352. color: var(--mint-text);
  353. line-height: 1.2;
  354. letter-spacing: -0.02em;
  355. font-variant-numeric: tabular-nums;
  356. }
  357. .stats-label {
  358. font-size: 12px;
  359. color: var(--mint-text-muted);
  360. margin-top: 4px;
  361. }
  362. }
  363. }
  364. .todo-card {
  365. padding: 14px;
  366. border-left: 3px solid var(--mint-border);
  367. cursor: pointer;
  368. transition: 0.2s ease;
  369. &:hover {
  370. border-color: var(--mint-primary);
  371. transform: none;
  372. box-shadow: var(--mint-shadow);
  373. }
  374. &.warning {
  375. border-left-color: #d97706;
  376. }
  377. &.danger {
  378. border-left-color: #dc2626;
  379. }
  380. &.primary {
  381. border-left-color: var(--mint-primary-dark);
  382. }
  383. &.success {
  384. border-left-color: var(--mint-primary);
  385. }
  386. .todo-header {
  387. display: flex;
  388. justify-content: space-between;
  389. align-items: center;
  390. margin-bottom: 12px;
  391. .todo-title {
  392. font-size: 14px;
  393. font-weight: 600;
  394. color: var(--mint-text);
  395. }
  396. .todo-count {
  397. background: var(--mint-primary-bg);
  398. color: var(--mint-primary-dark);
  399. padding: 2px 10px;
  400. border-radius: 6px;
  401. font-size: 12px;
  402. font-weight: 600;
  403. &.blue {
  404. background: var(--mint-primary-bg);
  405. color: var(--mint-primary-dark);
  406. }
  407. &.orange {
  408. background: #fffbeb;
  409. color: #d97706;
  410. }
  411. &.green {
  412. background: #d1fae5;
  413. color: #047857;
  414. }
  415. }
  416. }
  417. .todo-list {
  418. .todo-item {
  419. display: flex;
  420. justify-content: space-between;
  421. align-items: center;
  422. padding: 8px 0;
  423. border-bottom: 1px solid #edf4f1;
  424. font-size: 13px;
  425. &:last-child {
  426. border-bottom: none;
  427. }
  428. .todo-item-text {
  429. color: var(--mint-text-muted);
  430. flex: 1;
  431. overflow: hidden;
  432. text-overflow: ellipsis;
  433. white-space: nowrap;
  434. }
  435. .todo-item-action {
  436. color: var(--mint-primary-dark);
  437. cursor: pointer;
  438. margin-left: 8px;
  439. &:hover {
  440. text-decoration: underline;
  441. }
  442. }
  443. }
  444. }
  445. }
  446. .quick-btn {
  447. border-radius: var(--mint-radius);
  448. padding: 16px;
  449. display: flex;
  450. align-items: center;
  451. gap: 12px;
  452. cursor: pointer;
  453. transition: border-color 0.2s ease;
  454. &:hover {
  455. border-color: #b9dbcb;
  456. transform: none;
  457. box-shadow: var(--mint-shadow);
  458. }
  459. .quick-icon {
  460. width: 40px;
  461. height: 40px;
  462. border-radius: 10px;
  463. display: flex;
  464. align-items: center;
  465. justify-content: center;
  466. &.blue {
  467. background: var(--mint-primary-bg);
  468. color: var(--mint-primary-dark);
  469. }
  470. &.orange {
  471. background: #fffbeb;
  472. color: #d97706;
  473. }
  474. &.green {
  475. background: #d1fae5;
  476. color: #047857;
  477. }
  478. &.red {
  479. background: #fef2f2;
  480. color: #dc2626;
  481. }
  482. }
  483. .quick-text {
  484. font-size: 14px;
  485. font-weight: 500;
  486. color: var(--mint-text);
  487. flex: 1;
  488. }
  489. .quick-arrow {
  490. color: var(--mint-text-muted);
  491. }
  492. }
  493. .orders-card {
  494. .list-body {
  495. border-top: 1px solid var(--mint-border);
  496. }
  497. :deep(.el-table__cell .cell) {
  498. white-space: nowrap;
  499. }
  500. .amount-text {
  501. color: var(--mint-primary-dark);
  502. font-weight: 600;
  503. }
  504. }
  505. </style>