index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <template>
  2. <div class="sales-page">
  3. <section class="header-bar">
  4. <h1 class="header-title">数据监控</h1>
  5. <div class="header-actions">
  6. <el-upload action="" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls,.csv" :on-change="handleFileChange">
  7. <el-button size="small" icon="el-icon-upload2">上传销售数据</el-button>
  8. </el-upload>
  9. <el-button size="small" icon="el-icon-refresh" :loading="loading" @click="refreshData">刷新</el-button>
  10. <el-button size="small" plain icon="el-icon-setting" @click="settingsVisible = true">设置</el-button>
  11. <el-button size="small" type="primary" icon="el-icon-download" @click="exportData">导出</el-button>
  12. </div>
  13. </section>
  14. <section class="panel">
  15. <h3>数据同步状态</h3>
  16. <div class="platform-grid">
  17. <div v-for="platform in platforms" :key="platform.name" class="platform-card" :class="platform.status">
  18. <strong>{{ platform.name }}</strong>
  19. <i :class="platform.icon"></i>
  20. <span>{{ platform.label }}</span>
  21. <b>{{ platform.rate }}</b>
  22. <em>最后同步:{{ platform.lastSync }}</em>
  23. </div>
  24. </div>
  25. <div class="log-section">
  26. <div class="log-header"><span>同步日志</span><small>最近 {{ logs.length }} 条记录</small></div>
  27. <div v-for="log in logs" :key="log.id" class="log-item" :class="{ expanded: expandedLog === log.id }" @click="expandedLog = expandedLog === log.id ? '' : log.id">
  28. <div class="log-line">
  29. <i :class="log.icon"></i>
  30. <span>{{ log.title }}</span>
  31. <time>{{ log.time }}</time>
  32. </div>
  33. <p v-if="expandedLog === log.id">{{ log.detail }}</p>
  34. </div>
  35. </div>
  36. </section>
  37. <section class="overview-grid">
  38. <div class="overview-card">
  39. <div>
  40. <span>今日销售额</span>
  41. <strong>{{ formatMoney(todayRevenue) }}</strong>
  42. <em class="up">+{{ revenueGrowth }}% 较昨日</em>
  43. </div>
  44. <i class="el-icon-money sales"></i>
  45. </div>
  46. <div class="overview-card">
  47. <div>
  48. <span>今日订单量</span>
  49. <strong>{{ formatNumber(todayOrders) }}</strong>
  50. <em class="up">+{{ orderGrowth }}% 较昨日</em>
  51. </div>
  52. <i class="el-icon-s-order orders"></i>
  53. </div>
  54. </section>
  55. <section class="panel">
  56. <div class="panel-head">
  57. <h3>数据源配置</h3>
  58. <el-button size="small" type="primary" icon="el-icon-plus" @click="addSource">添加数据源</el-button>
  59. </div>
  60. <el-table :data="platforms" size="small">
  61. <el-table-column prop="name" label="平台名称" min-width="120" />
  62. <el-table-column label="API 状态" min-width="120">
  63. <template slot-scope="{ row }"><span class="status-dot" :class="row.status"></span> {{ row.label }}</template>
  64. </el-table-column>
  65. <el-table-column prop="frequency" label="同步频率" min-width="120" />
  66. <el-table-column prop="lastSync" label="最后同步时间" min-width="160" />
  67. <el-table-column label="操作" min-width="180">
  68. <template slot-scope="{ row }">
  69. <el-button size="mini" type="success" :disabled="row.status === 'syncing'" @click="syncPlatform(row)">立即同步</el-button>
  70. <el-button size="mini" plain @click="editPlatform(row)">编辑配置</el-button>
  71. </template>
  72. </el-table-column>
  73. </el-table>
  74. </section>
  75. <el-dialog title="系统设置" :visible.sync="settingsVisible" width="480px">
  76. <el-form label-position="top">
  77. <el-form-item label="全局同步频率"><el-select v-model="settings.frequency"><el-option label="每 5 分钟" value="5" /><el-option label="每 30 分钟" value="30" /><el-option label="每小时" value="60" /></el-select></el-form-item>
  78. <el-form-item label="失败重试次数"><el-input-number v-model="settings.retry" :min="1" :max="10" /></el-form-item>
  79. <el-form-item label="销售额下降告警阈值"><el-input-number v-model="settings.warning" :min="1" :max="100" /> %</el-form-item>
  80. </el-form>
  81. <span slot="footer"><el-button @click="settingsVisible = false">取消</el-button><el-button type="primary" @click="saveSettings">保存设置</el-button></span>
  82. </el-dialog>
  83. </div>
  84. </template>
  85. <script>
  86. import { analyzeSaleOverviewWithFile, getSaleOverviewResults } from '@/api/client'
  87. export default {
  88. name: 'SaleDataMonitor',
  89. data() {
  90. return {
  91. loading: false,
  92. file: null,
  93. results: {},
  94. expandedLog: '',
  95. settingsVisible: false,
  96. settings: { frequency: '5', retry: 5, warning: 20 },
  97. platforms: [
  98. { name: '天猫', status: 'online', label: '在线', icon: 'el-icon-success', rate: '1,280 单/小时', lastSync: '刚刚', frequency: '每 5 分钟' },
  99. { name: '京东', status: 'syncing', label: '同步中', icon: 'el-icon-loading', rate: '856 单/小时', lastSync: '2 分钟前', frequency: '每 10 分钟' },
  100. { name: '抖音', status: 'online', label: '在线', icon: 'el-icon-success', rate: '2,340 单/小时', lastSync: '刚刚', frequency: '每 10 分钟' },
  101. { name: '拼多多', status: 'error', label: '异常', icon: 'el-icon-error', rate: '0 单/小时', lastSync: '1 小时前', frequency: '每 30 分钟' },
  102. { name: '快手', status: 'offline', label: '未连接', icon: 'el-icon-remove', rate: '0 单/小时', lastSync: '3 天前', frequency: '每 30 分钟' }
  103. ],
  104. logs: [
  105. { id: '1', icon: 'el-icon-success', title: '天猫 - 同步完成', time: '14:32:15', detail: '同步数据量:2,845 条 | 耗时:4.2 秒 | 成功率:100%' },
  106. { id: '2', icon: 'el-icon-loading', title: '京东 - 正在同步', time: '14:31:48', detail: '正在同步第 1,568/3,200 条数据' },
  107. { id: '3', icon: 'el-icon-warning-outline', title: '拼多多 - 同步失败:API 连接超时', time: '14:28:15', detail: '连接超时 30 秒 | 重试次数:3/5 | 下次重试:5 分钟后' }
  108. ]
  109. }
  110. },
  111. computed: {
  112. summary() {
  113. return this.results.summary || {}
  114. },
  115. todayRevenue() {
  116. return Number(this.summary.total_revenue || 128560)
  117. },
  118. todayOrders() {
  119. return Number(this.summary.total_orders || 2340)
  120. },
  121. revenueGrowth() {
  122. return Number(this.summary.revenue_growth || 15.3).toFixed(1)
  123. },
  124. orderGrowth() {
  125. return Number(this.summary.order_growth || 8.7).toFixed(1)
  126. }
  127. },
  128. mounted() {
  129. this.loadCachedResults()
  130. },
  131. methods: {
  132. handleFileChange(file) {
  133. this.file = file.raw
  134. this.refreshData()
  135. },
  136. async loadCachedResults() {
  137. try {
  138. const response = await getSaleOverviewResults()
  139. if (response && response.success) this.results = response.data || {}
  140. } catch (e) {
  141. this.results = {}
  142. }
  143. },
  144. async refreshData() {
  145. this.loading = true
  146. try {
  147. if (this.file) {
  148. const response = await analyzeSaleOverviewWithFile(this.file)
  149. if (!response.success) throw new Error(response.message || '分析失败')
  150. this.results = response.data || {}
  151. this.$modal.msgSuccess('数据已刷新')
  152. } else {
  153. await this.loadCachedResults()
  154. if (Object.keys(this.results || {}).length) {
  155. this.$modal.msgSuccess('已加载缓存数据')
  156. } else {
  157. this.$modal.msgWarning('暂无缓存数据,请先上传销售数据')
  158. }
  159. }
  160. } catch (error) {
  161. this.$modal.msgError(error.message || '刷新失败')
  162. } finally {
  163. this.loading = false
  164. }
  165. },
  166. syncPlatform(row) {
  167. this.$modal.msgSuccess(`开始同步 ${row.name}`)
  168. },
  169. editPlatform(row) {
  170. this.$modal.msg(`编辑 ${row.name} 配置`)
  171. },
  172. addSource() {
  173. this.$modal.msg('数据源配置入口已打开')
  174. },
  175. saveSettings() {
  176. this.settingsVisible = false
  177. this.$modal.msgSuccess('设置已保存')
  178. },
  179. exportData() {
  180. const blob = new Blob([JSON.stringify(this.results, null, 2)], { type: 'application/json;charset=utf-8' })
  181. const url = URL.createObjectURL(blob)
  182. const link = document.createElement('a')
  183. link.href = url
  184. link.download = `sale_monitor_${Date.now()}.json`
  185. link.click()
  186. URL.revokeObjectURL(url)
  187. },
  188. formatMoney(value) { return `¥${Math.round(Number(value || 0)).toLocaleString()}` },
  189. formatNumber(value) { return Math.round(Number(value || 0)).toLocaleString() }
  190. }
  191. }
  192. </script>
  193. <style scoped lang="scss">
  194. .sales-page { padding: 20px; max-width: 1400px; margin: 0 auto; background: #f0f2f5; min-height: calc(100vh - 84px); }
  195. .header-bar, .panel, .overview-card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
  196. .header-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; margin-bottom: 20px; gap: 16px; flex-wrap: wrap; }
  197. .header-title { margin: 0; font-size: 20px; font-weight: 600; color: #303133; }
  198. .header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
  199. .panel { padding: 24px; margin-bottom: 20px; }
  200. .panel h3 { margin: 0 0 20px; font-size: 16px; color: #303133; }
  201. .platform-grid { display: grid; grid-template-columns: repeat(5,1fr); gap: 15px; }
  202. .platform-card { background: #f8f9fa; border-left: 4px solid #909399; border-radius: 8px; padding: 18px; text-align: center; display: flex; flex-direction: column; gap: 8px; }
  203. .platform-card.online { border-left-color: #67c23a; }
  204. .platform-card.syncing { border-left-color: #e6a23c; }
  205. .platform-card.error { border-left-color: #f56c6c; }
  206. .platform-card i { font-size: 24px; }
  207. .platform-card span, .platform-card em { color: #909399; font-size: 12px; font-style: normal; }
  208. .log-section { margin-top: 22px; padding-top: 18px; border-top: 1px solid #f0f0f0; }
  209. .log-header { display: flex; justify-content: space-between; color: #606266; margin-bottom: 12px; }
  210. .log-item { background: #f8f9fa; border-radius: 8px; padding: 12px 15px; margin-bottom: 8px; cursor: pointer; }
  211. .log-item.expanded { background: #e8f4f8; border-left: 3px solid #17a2b8; }
  212. .log-line { display: flex; align-items: center; gap: 10px; }
  213. .log-line span { flex: 1; }
  214. .log-line time { color: #909399; font-size: 12px; }
  215. .log-item p { margin: 10px 0 0 26px; color: #606266; font-size: 12px; }
  216. .overview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
  217. .overview-card { padding: 24px; display: flex; justify-content: space-between; align-items: center; }
  218. .overview-card span { color: #909399; font-size: 13px; }
  219. .overview-card strong { display: block; font-size: 34px; margin: 8px 0; }
  220. .overview-card em { font-style: normal; }
  221. .overview-card > i { width: 60px; height: 60px; border-radius: 15px; display: flex; align-items: center; justify-content: center; font-size: 28px; }
  222. .overview-card .sales { background: rgba(102,126,234,.12); color: #667eea; }
  223. .overview-card .orders { background: rgba(103,194,58,.12); color: #67c23a; }
  224. .up { color: #67c23a; }
  225. .panel-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
  226. .panel-head h3 { margin: 0; }
  227. .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; background: #909399; }
  228. .status-dot.online { background: #67c23a; }
  229. .status-dot.syncing { background: #e6a23c; }
  230. .status-dot.error { background: #f56c6c; }
  231. @media (max-width: 1200px) { .platform-grid { grid-template-columns: repeat(3,1fr); } }
  232. @media (max-width: 800px) { .platform-grid, .overview-grid { grid-template-columns: 1fr; } }
  233. </style>