PriceWatchView.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <template>
  2. <div class="app-page">
  3. <section class="glass-card section-card">
  4. <el-form :model="filters" inline class="filter-form">
  5. <el-form-item label="SKU">
  6. <el-input v-model="filters.sku" placeholder="SKU编号" clearable style="width:140px" @keyup.enter="loadData" />
  7. </el-form-item>
  8. <el-form-item label="商品名称">
  9. <el-input v-model="filters.productTitle" placeholder="商品名称" clearable style="width:160px" />
  10. </el-form-item>
  11. <el-form-item label="渠道">
  12. <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
  13. <el-option label="Shopify" value="Shopify" />
  14. <el-option label="TikTok Shop" value="TikTok Shop" />
  15. <el-option label="Amazon" value="Amazon" />
  16. </el-select>
  17. </el-form-item>
  18. <el-form-item label="预警状态">
  19. <el-select v-model="filters.alertStatus" placeholder="全部" clearable style="width:120px">
  20. <el-option label="正常" value="正常" />
  21. <el-option label="价格高" value="价格高" />
  22. <el-option label="价格低" value="价格低" />
  23. <el-option label="价差大" value="价差大" />
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item>
  27. <el-button type="primary" @click="loadData">查询</el-button>
  28. <el-button @click="resetFilters">重置</el-button>
  29. </el-form-item>
  30. </el-form>
  31. </section>
  32. <section class="glass-card section-card" style="padding:12px 24px">
  33. <div class="table-toolbar" style="margin-bottom:0">
  34. <div class="chip-list">
  35. <el-button @click="openConfig">价格源配置</el-button>
  36. <el-button @click="doExport">导出</el-button>
  37. </div>
  38. <el-select v-model="alertThreshold" placeholder="价差阈值" clearable style="width:160px">
  39. <el-option label="价差 > 10%" value="10" />
  40. <el-option label="价差 > 20%" value="20" />
  41. <el-option label="价差 > 30%" value="30" />
  42. </el-select>
  43. </div>
  44. </section>
  45. <section class="glass-card section-card">
  46. <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading" @selection-change="onSelection">
  47. <el-table-column type="selection" width="45" />
  48. <el-table-column prop="sku" label="SKU" width="130" />
  49. <el-table-column prop="productTitle" label="商品标题" min-width="200" show-overflow-tooltip />
  50. <el-table-column prop="channel" label="渠道" width="110" />
  51. <el-table-column prop="shopName" label="店铺" width="140" />
  52. <el-table-column prop="localPrice" label="本系统价格" width="110">
  53. <template #default="{ row }">
  54. <span style="font-weight:600">${{ row.localPrice }}</span>
  55. </template>
  56. </el-table-column>
  57. <el-table-column prop="competitorPrice" label="竞品价格" width="110">
  58. <template #default="{ row }">
  59. <span>${{ row.competitorPrice }}</span>
  60. </template>
  61. </el-table-column>
  62. <el-table-column prop="priceDiff" label="价差" width="80">
  63. <template #default="{ row }">
  64. <span :style="{ color: row.priceDiff > 0 ? 'var(--cb-danger)' : 'var(--cb-success)' }">
  65. {{ row.priceDiff > 0 ? '+' : '' }}{{ row.priceDiff }}%
  66. </span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column prop="competitorName" label="竞品来源" width="120" show-overflow-tooltip />
  70. <el-table-column prop="lastUpdate" label="更新时间" width="160" />
  71. <el-table-column prop="alertStatus" label="预警状态" width="100">
  72. <template #default="{ row }">
  73. <el-tag :type="alertTag(row.alertStatus)" size="small">{{ row.alertStatus }}</el-tag>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="操作" width="160" fixed="right">
  77. <template #default="{ row }">
  78. <el-button link type="primary" @click="openDetail(row)">详情</el-button>
  79. <el-button link type="primary" @click="adjustPrice(row)">调价</el-button>
  80. <el-button link type="primary" @click="muteAlert(row)">忽略</el-button>
  81. </template>
  82. </el-table-column>
  83. <template #empty>
  84. <el-empty description="暂无数据" />
  85. </template>
  86. </el-table>
  87. </section>
  88. <section class="glass-card section-card" style="padding:16px">
  89. <h4 style="margin:0 0 12px">价格监控汇总</h4>
  90. <div class="stat-grid" style="grid-template-columns:repeat(4, 1fr)">
  91. <article class="stat-card">
  92. <div class="stat-card__label">监控商品数</div>
  93. <div class="stat-card__value" style="font-size:24px;color:var(--cb-primary)">1,258</div>
  94. </article>
  95. <article class="stat-card">
  96. <div class="stat-card__label">价格异常数</div>
  97. <div class="stat-card__value" style="font-size:24px;color:var(--cb-danger)">42</div>
  98. </article>
  99. <article class="stat-card">
  100. <div class="stat-card__label">本周调价次数</div>
  101. <div class="stat-card__value" style="font-size:24px">128</div>
  102. </article>
  103. <article class="stat-card">
  104. <div class="stat-card__label">平均价差</div>
  105. <div class="stat-card__value" style="font-size:24px">+5.2%</div>
  106. </article>
  107. </div>
  108. </section>
  109. <el-dialog v-model="configVisible" title="价格源配置" width="600px">
  110. <el-form :model="configForm" label-width="100px">
  111. <el-form-item label="竞品来源">
  112. <el-input v-model="configForm.sourceName" placeholder="如:Amazon官方旗舰店" style="width:100%" />
  113. </el-form-item>
  114. <el-form-item label="来源URL">
  115. <el-input v-model="configForm.sourceUrl" placeholder="请输入竞品页面URL" style="width:100%" />
  116. </el-form-item>
  117. <el-form-item label="刷新频率">
  118. <el-select v-model="configForm.refreshRate" style="width:100%">
  119. <el-option label="每小时" value="1h" />
  120. <el-option label="每6小时" value="6h" />
  121. <el-option label="每天" value="1d" />
  122. <el-option label="每周" value="1w" />
  123. </el-select>
  124. </el-form-item>
  125. <el-form-item label="爬虫规则">
  126. <el-input v-model="configForm.selector" placeholder="CSS选择器或XPath" style="width:100%" />
  127. </el-form-item>
  128. <el-form-item label="启用状态">
  129. <el-switch v-model="configForm.enabled" />
  130. </el-form-item>
  131. </el-form>
  132. <template #footer>
  133. <el-button @click="configVisible = false">取消</el-button>
  134. <el-button type="primary" @click="submitConfig">保存</el-button>
  135. </template>
  136. </el-dialog>
  137. <el-dialog v-model="adjustVisible" title="价格调整" width="480px">
  138. <el-form :model="adjustForm" label-width="90px">
  139. <el-form-item label="SKU">
  140. <el-input v-model="adjustForm.sku" disabled />
  141. </el-form-item>
  142. <el-form-item label="当前价格">
  143. <el-input v-model="adjustForm.currentPrice" disabled />
  144. </el-form-item>
  145. <el-form-item label="竞品价格">
  146. <el-input v-model="adjustForm.competitorPrice" disabled />
  147. </el-form-item>
  148. <el-form-item label="调整方式">
  149. <el-radio-group v-model="adjustForm.adjustType">
  150. <el-radio label="fixed">固定价格</el-radio>
  151. <el-radio label="formula">按公式</el-radio>
  152. </el-radio-group>
  153. </el-form-item>
  154. <el-form-item label="新价格" v-if="adjustForm.adjustType === 'fixed'">
  155. <el-input-number v-model="adjustForm.newPrice" :min="0" :precision="2" style="width:100%" />
  156. </el-form-item>
  157. <el-form-item label="公式" v-else>
  158. <el-input v-model="adjustForm.formula" placeholder="如:竞品价格 × 0.95" style="width:100%" />
  159. <span style="color:var(--cb-text-secondary);font-size:12px;margin-top:4px">支持变量:competitor_price, cost_price, exchange_rate</span>
  160. </el-form-item>
  161. <el-form-item label="生效时间">
  162. <el-date-picker v-model="adjustForm.effectiveTime" type="datetime" placeholder="立即生效" style="width:100%" />
  163. </el-form-item>
  164. </el-form>
  165. <template #footer>
  166. <el-button @click="adjustVisible = false">取消</el-button>
  167. <el-button type="primary" @click="submitAdjust">确认调整</el-button>
  168. </template>
  169. </el-dialog>
  170. <el-dialog v-model="detailVisible" title="价格详情" width="560px">
  171. <el-descriptions :column="1" border v-if="detailItem">
  172. <el-descriptions-item label="SKU">{{ detailItem.sku }}</el-descriptions-item>
  173. <el-descriptions-item label="商品标题">{{ detailItem.productTitle }}</el-descriptions-item>
  174. <el-descriptions-item label="渠道">{{ detailItem.channel }}</el-descriptions-item>
  175. <el-descriptions-item label="店铺">{{ detailItem.shopName }}</el-descriptions-item>
  176. <el-descriptions-item label="本系统价格">${{ detailItem.localPrice }}</el-descriptions-item>
  177. <el-descriptions-item label="竞品价格">${{ detailItem.competitorPrice }}</el-descriptions-item>
  178. <el-descriptions-item label="价差">{{ detailItem.priceDiff }}%</el-descriptions-item>
  179. <el-descriptions-item label="竞品来源">{{ detailItem.competitorName }}</el-descriptions-item>
  180. <el-descriptions-item label="竞品URL">{{ detailItem.competitorUrl }}</el-descriptions-item>
  181. <el-descriptions-item label="最后更新">{{ detailItem.lastUpdate }}</el-descriptions-item>
  182. <el-descriptions-item label="预警状态">
  183. <el-tag :type="alertTag(detailItem.alertStatus)">{{ detailItem.alertStatus }}</el-tag>
  184. </el-descriptions-item>
  185. </el-descriptions>
  186. <template #footer>
  187. <el-button @click="detailVisible = false">关闭</el-button>
  188. </template>
  189. </el-dialog>
  190. </div>
  191. </template>
  192. <script setup lang="ts">
  193. import { computed, onMounted, ref } from 'vue';
  194. import { ElMessage } from 'element-plus';
  195. interface PriceWatchItem {
  196. id: string;
  197. sku: string;
  198. productTitle: string;
  199. channel: string;
  200. shopName: string;
  201. localPrice: string;
  202. competitorPrice: string;
  203. priceDiff: number;
  204. competitorName: string;
  205. competitorUrl: string;
  206. lastUpdate: string;
  207. alertStatus: string;
  208. }
  209. const items = ref<PriceWatchItem[]>([
  210. { id: 'PW001', sku: 'SKU-NM-BK-M', productTitle: 'Nomad 防水背包 黑色 中号', channel: 'Shopify', shopName: 'NomadPeak US', localPrice: '89.99', competitorPrice: '79.99', priceDiff: 12.5, competitorName: 'Amazon - TrailGear Official', competitorUrl: 'https://amazon.com/dp/B08XXXX', lastUpdate: '2026-04-20 10:30:15', alertStatus: '价格高' },
  211. { id: 'PW002', sku: 'SKU-AE-GR-L', productTitle: 'AeroDry 速干T恤 绿色 L码', channel: 'TikTok Shop', shopName: 'AeroDry UK', localPrice: '24.99', competitorPrice: '27.99', priceDiff: -10.7, competitorName: 'TikTok Shop - SportsWorld', competitorUrl: 'https://tiktok.com/product/123', lastUpdate: '2026-04-20 09:15:22', alertStatus: '价格低' },
  212. { id: 'PW003', sku: 'SKU-UT-WH-XL', productTitle: 'UrbanTrail 徒步鞋 白色 XL', channel: 'Amazon', shopName: 'UrbanTrail NA', localPrice: '129.00', competitorPrice: '119.00', priceDiff: 8.4, competitorName: 'Amazon - HikePro Store', competitorUrl: 'https://amazon.com/dp/B09YYYY', lastUpdate: '2026-04-20 08:45:33', alertStatus: '价差大' },
  213. { id: 'PW004', sku: 'SKU-NM-BK-S', productTitle: 'Nomad 防水背包 黑色 小号', channel: 'Shopify', shopName: 'NomadPeak US', localPrice: '69.99', competitorPrice: '69.99', priceDiff: 0, competitorName: 'Amazon - TrailGear Official', competitorUrl: 'https://amazon.com/dp/B08ZZZZ', lastUpdate: '2026-04-19 22:20:45', alertStatus: '正常' },
  214. { id: 'PW005', sku: 'SKU-AE-BL-M', productTitle: 'AeroDry 运动短裤 蓝色 M码', channel: 'TikTok Shop', shopName: 'AeroDry UK', localPrice: '19.99', competitorPrice: '22.99', priceDiff: -13.0, competitorName: 'Shopify - SportsLife', competitorUrl: 'https://sportslife.com/p/456', lastUpdate: '2026-04-19 18:35:08', alertStatus: '价格低' },
  215. { id: 'PW006', sku: 'SKU-UT-BK-42', productTitle: 'UrbanTrail 登山靴 黑色 42码', channel: 'Amazon', shopName: 'UrbanTrail NA', localPrice: '189.00', competitorPrice: '199.00', priceDiff: -5.0, competitorName: 'Amazon - MountainGear', competitorUrl: 'https://amazon.com/dp/B07AAAA', lastUpdate: '2026-04-19 14:12:30', alertStatus: '正常' }
  216. ]);
  217. const loading = ref(false);
  218. const selected = ref<PriceWatchItem[]>([]);
  219. const alertThreshold = ref('');
  220. const configVisible = ref(false);
  221. const adjustVisible = ref(false);
  222. const detailVisible = ref(false);
  223. const detailItem = ref<PriceWatchItem | null>(null);
  224. const filters = ref({
  225. sku: '',
  226. productTitle: '',
  227. channel: '',
  228. alertStatus: ''
  229. });
  230. const configForm = ref({
  231. sourceName: '',
  232. sourceUrl: '',
  233. refreshRate: '1d',
  234. selector: '',
  235. enabled: true
  236. });
  237. const adjustForm = ref({
  238. sku: '',
  239. currentPrice: '',
  240. competitorPrice: '',
  241. adjustType: 'fixed',
  242. newPrice: 0,
  243. formula: '',
  244. effectiveTime: ''
  245. });
  246. const filteredItems = computed(() => {
  247. return items.value.filter(item => {
  248. if (filters.value.sku && !item.sku.includes(filters.value.sku)) return false;
  249. if (filters.value.productTitle && !item.productTitle.includes(filters.value.productTitle)) return false;
  250. if (filters.value.channel && item.channel !== filters.value.channel) return false;
  251. if (filters.value.alertStatus && item.alertStatus !== filters.value.alertStatus) return false;
  252. return true;
  253. });
  254. });
  255. const alertTag = (status: string) => {
  256. const map: Record<string, string> = { '正常': 'success', '价格高': 'danger', '价格低': 'success', '价差大': 'warning' };
  257. return map[status] || '';
  258. };
  259. const loadData = () => {
  260. loading.value = true;
  261. setTimeout(() => { loading.value = false; }, 300);
  262. };
  263. const resetFilters = () => {
  264. filters.value = { sku: '', productTitle: '', channel: '', alertStatus: '' };
  265. };
  266. const onSelection = (rows: PriceWatchItem[]) => { selected.value = rows; };
  267. const openConfig = () => {
  268. configForm.value = { sourceName: '', sourceUrl: '', refreshRate: '1d', selector: '', enabled: true };
  269. configVisible.value = true;
  270. };
  271. const submitConfig = () => {
  272. ElMessage.success('价格源配置已保存');
  273. configVisible.value = false;
  274. };
  275. const openDetail = (row: PriceWatchItem) => {
  276. detailItem.value = row;
  277. detailVisible.value = true;
  278. };
  279. const adjustPrice = (row: PriceWatchItem) => {
  280. adjustForm.value = {
  281. sku: row.sku,
  282. currentPrice: row.localPrice,
  283. competitorPrice: row.competitorPrice,
  284. adjustType: 'fixed',
  285. newPrice: parseFloat(row.competitorPrice) * 0.95,
  286. formula: `竞品价格 × 0.95 = $${(parseFloat(row.competitorPrice) * 0.95).toFixed(2)}`,
  287. effectiveTime: ''
  288. };
  289. adjustVisible.value = true;
  290. };
  291. const submitAdjust = () => {
  292. ElMessage.success('价格调整已提交');
  293. adjustVisible.value = false;
  294. };
  295. const muteAlert = (row: PriceWatchItem) => {
  296. ElMessage.success('已忽略该预警');
  297. };
  298. const doExport = () => {
  299. ElMessage.info('导出开始,完成后将自动下载');
  300. };
  301. onMounted(loadData);
  302. </script>
  303. <style scoped>
  304. .filter-form :deep(.el-form-item) { margin-bottom: 0; }
  305. </style>