| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- <template>
- <div class="app-page">
- <section class="glass-card section-card">
- <el-form :model="filters" inline class="filter-form">
- <el-form-item label="SKU">
- <el-input v-model="filters.sku" placeholder="SKU编号" clearable style="width:140px" @keyup.enter="loadData" />
- </el-form-item>
- <el-form-item label="商品名称">
- <el-input v-model="filters.productTitle" placeholder="商品名称" clearable style="width:160px" />
- </el-form-item>
- <el-form-item label="渠道">
- <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
- <el-option label="Shopify" value="Shopify" />
- <el-option label="TikTok Shop" value="TikTok Shop" />
- <el-option label="Amazon" value="Amazon" />
- </el-select>
- </el-form-item>
- <el-form-item label="预警状态">
- <el-select v-model="filters.alertStatus" placeholder="全部" clearable style="width:120px">
- <el-option label="正常" value="正常" />
- <el-option label="价格高" value="价格高" />
- <el-option label="价格低" value="价格低" />
- <el-option label="价差大" value="价差大" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="loadData">查询</el-button>
- <el-button @click="resetFilters">重置</el-button>
- </el-form-item>
- </el-form>
- </section>
- <section class="glass-card section-card" style="padding:12px 24px">
- <div class="table-toolbar" style="margin-bottom:0">
- <div class="chip-list">
- <el-button @click="openConfig">价格源配置</el-button>
- <el-button @click="doExport">导出</el-button>
- </div>
- <el-select v-model="alertThreshold" placeholder="价差阈值" clearable style="width:160px">
- <el-option label="价差 > 10%" value="10" />
- <el-option label="价差 > 20%" value="20" />
- <el-option label="价差 > 30%" value="30" />
- </el-select>
- </div>
- </section>
- <section class="glass-card section-card">
- <el-table :data="filteredItems" stripe style="width:100%" v-loading="loading" @selection-change="onSelection">
- <el-table-column type="selection" width="45" />
- <el-table-column prop="sku" label="SKU" width="130" />
- <el-table-column prop="productTitle" label="商品标题" min-width="200" show-overflow-tooltip />
- <el-table-column prop="channel" label="渠道" width="110" />
- <el-table-column prop="shopName" label="店铺" width="140" />
- <el-table-column prop="localPrice" label="本系统价格" width="110">
- <template #default="{ row }">
- <span style="font-weight:600">${{ row.localPrice }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="competitorPrice" label="竞品价格" width="110">
- <template #default="{ row }">
- <span>${{ row.competitorPrice }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="priceDiff" label="价差" width="80">
- <template #default="{ row }">
- <span :style="{ color: row.priceDiff > 0 ? 'var(--cb-danger)' : 'var(--cb-success)' }">
- {{ row.priceDiff > 0 ? '+' : '' }}{{ row.priceDiff }}%
- </span>
- </template>
- </el-table-column>
- <el-table-column prop="competitorName" label="竞品来源" width="120" show-overflow-tooltip />
- <el-table-column prop="lastUpdate" label="更新时间" width="160" />
- <el-table-column prop="alertStatus" label="预警状态" width="100">
- <template #default="{ row }">
- <el-tag :type="alertTag(row.alertStatus)" size="small">{{ row.alertStatus }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="160" fixed="right">
- <template #default="{ row }">
- <el-button link type="primary" @click="openDetail(row)">详情</el-button>
- <el-button link type="primary" @click="adjustPrice(row)">调价</el-button>
- <el-button link type="primary" @click="muteAlert(row)">忽略</el-button>
- </template>
- </el-table-column>
- <template #empty>
- <el-empty description="暂无数据" />
- </template>
- </el-table>
- </section>
- <section class="glass-card section-card" style="padding:16px">
- <h4 style="margin:0 0 12px">价格监控汇总</h4>
- <div class="stat-grid" style="grid-template-columns:repeat(4, 1fr)">
- <article class="stat-card">
- <div class="stat-card__label">监控商品数</div>
- <div class="stat-card__value" style="font-size:24px;color:var(--cb-primary)">1,258</div>
- </article>
- <article class="stat-card">
- <div class="stat-card__label">价格异常数</div>
- <div class="stat-card__value" style="font-size:24px;color:var(--cb-danger)">42</div>
- </article>
- <article class="stat-card">
- <div class="stat-card__label">本周调价次数</div>
- <div class="stat-card__value" style="font-size:24px">128</div>
- </article>
- <article class="stat-card">
- <div class="stat-card__label">平均价差</div>
- <div class="stat-card__value" style="font-size:24px">+5.2%</div>
- </article>
- </div>
- </section>
- <el-dialog v-model="configVisible" title="价格源配置" width="600px">
- <el-form :model="configForm" label-width="100px">
- <el-form-item label="竞品来源">
- <el-input v-model="configForm.sourceName" placeholder="如:Amazon官方旗舰店" style="width:100%" />
- </el-form-item>
- <el-form-item label="来源URL">
- <el-input v-model="configForm.sourceUrl" placeholder="请输入竞品页面URL" style="width:100%" />
- </el-form-item>
- <el-form-item label="刷新频率">
- <el-select v-model="configForm.refreshRate" style="width:100%">
- <el-option label="每小时" value="1h" />
- <el-option label="每6小时" value="6h" />
- <el-option label="每天" value="1d" />
- <el-option label="每周" value="1w" />
- </el-select>
- </el-form-item>
- <el-form-item label="爬虫规则">
- <el-input v-model="configForm.selector" placeholder="CSS选择器或XPath" style="width:100%" />
- </el-form-item>
- <el-form-item label="启用状态">
- <el-switch v-model="configForm.enabled" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="configVisible = false">取消</el-button>
- <el-button type="primary" @click="submitConfig">保存</el-button>
- </template>
- </el-dialog>
- <el-dialog v-model="adjustVisible" title="价格调整" width="480px">
- <el-form :model="adjustForm" label-width="90px">
- <el-form-item label="SKU">
- <el-input v-model="adjustForm.sku" disabled />
- </el-form-item>
- <el-form-item label="当前价格">
- <el-input v-model="adjustForm.currentPrice" disabled />
- </el-form-item>
- <el-form-item label="竞品价格">
- <el-input v-model="adjustForm.competitorPrice" disabled />
- </el-form-item>
- <el-form-item label="调整方式">
- <el-radio-group v-model="adjustForm.adjustType">
- <el-radio label="fixed">固定价格</el-radio>
- <el-radio label="formula">按公式</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="新价格" v-if="adjustForm.adjustType === 'fixed'">
- <el-input-number v-model="adjustForm.newPrice" :min="0" :precision="2" style="width:100%" />
- </el-form-item>
- <el-form-item label="公式" v-else>
- <el-input v-model="adjustForm.formula" placeholder="如:竞品价格 × 0.95" style="width:100%" />
- <span style="color:var(--cb-text-secondary);font-size:12px;margin-top:4px">支持变量:competitor_price, cost_price, exchange_rate</span>
- </el-form-item>
- <el-form-item label="生效时间">
- <el-date-picker v-model="adjustForm.effectiveTime" type="datetime" placeholder="立即生效" style="width:100%" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="adjustVisible = false">取消</el-button>
- <el-button type="primary" @click="submitAdjust">确认调整</el-button>
- </template>
- </el-dialog>
- <el-dialog v-model="detailVisible" title="价格详情" width="560px">
- <el-descriptions :column="1" border v-if="detailItem">
- <el-descriptions-item label="SKU">{{ detailItem.sku }}</el-descriptions-item>
- <el-descriptions-item label="商品标题">{{ detailItem.productTitle }}</el-descriptions-item>
- <el-descriptions-item label="渠道">{{ detailItem.channel }}</el-descriptions-item>
- <el-descriptions-item label="店铺">{{ detailItem.shopName }}</el-descriptions-item>
- <el-descriptions-item label="本系统价格">${{ detailItem.localPrice }}</el-descriptions-item>
- <el-descriptions-item label="竞品价格">${{ detailItem.competitorPrice }}</el-descriptions-item>
- <el-descriptions-item label="价差">{{ detailItem.priceDiff }}%</el-descriptions-item>
- <el-descriptions-item label="竞品来源">{{ detailItem.competitorName }}</el-descriptions-item>
- <el-descriptions-item label="竞品URL">{{ detailItem.competitorUrl }}</el-descriptions-item>
- <el-descriptions-item label="最后更新">{{ detailItem.lastUpdate }}</el-descriptions-item>
- <el-descriptions-item label="预警状态">
- <el-tag :type="alertTag(detailItem.alertStatus)">{{ detailItem.alertStatus }}</el-tag>
- </el-descriptions-item>
- </el-descriptions>
- <template #footer>
- <el-button @click="detailVisible = false">关闭</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, ref } from 'vue';
- import { ElMessage } from 'element-plus';
- interface PriceWatchItem {
- id: string;
- sku: string;
- productTitle: string;
- channel: string;
- shopName: string;
- localPrice: string;
- competitorPrice: string;
- priceDiff: number;
- competitorName: string;
- competitorUrl: string;
- lastUpdate: string;
- alertStatus: string;
- }
- const items = ref<PriceWatchItem[]>([
- { 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: '价格高' },
- { 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: '价格低' },
- { 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: '价差大' },
- { 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: '正常' },
- { 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: '价格低' },
- { 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: '正常' }
- ]);
- const loading = ref(false);
- const selected = ref<PriceWatchItem[]>([]);
- const alertThreshold = ref('');
- const configVisible = ref(false);
- const adjustVisible = ref(false);
- const detailVisible = ref(false);
- const detailItem = ref<PriceWatchItem | null>(null);
- const filters = ref({
- sku: '',
- productTitle: '',
- channel: '',
- alertStatus: ''
- });
- const configForm = ref({
- sourceName: '',
- sourceUrl: '',
- refreshRate: '1d',
- selector: '',
- enabled: true
- });
- const adjustForm = ref({
- sku: '',
- currentPrice: '',
- competitorPrice: '',
- adjustType: 'fixed',
- newPrice: 0,
- formula: '',
- effectiveTime: ''
- });
- const filteredItems = computed(() => {
- return items.value.filter(item => {
- if (filters.value.sku && !item.sku.includes(filters.value.sku)) return false;
- if (filters.value.productTitle && !item.productTitle.includes(filters.value.productTitle)) return false;
- if (filters.value.channel && item.channel !== filters.value.channel) return false;
- if (filters.value.alertStatus && item.alertStatus !== filters.value.alertStatus) return false;
- return true;
- });
- });
- const alertTag = (status: string) => {
- const map: Record<string, string> = { '正常': 'success', '价格高': 'danger', '价格低': 'success', '价差大': 'warning' };
- return map[status] || '';
- };
- const loadData = () => {
- loading.value = true;
- setTimeout(() => { loading.value = false; }, 300);
- };
- const resetFilters = () => {
- filters.value = { sku: '', productTitle: '', channel: '', alertStatus: '' };
- };
- const onSelection = (rows: PriceWatchItem[]) => { selected.value = rows; };
- const openConfig = () => {
- configForm.value = { sourceName: '', sourceUrl: '', refreshRate: '1d', selector: '', enabled: true };
- configVisible.value = true;
- };
- const submitConfig = () => {
- ElMessage.success('价格源配置已保存');
- configVisible.value = false;
- };
- const openDetail = (row: PriceWatchItem) => {
- detailItem.value = row;
- detailVisible.value = true;
- };
- const adjustPrice = (row: PriceWatchItem) => {
- adjustForm.value = {
- sku: row.sku,
- currentPrice: row.localPrice,
- competitorPrice: row.competitorPrice,
- adjustType: 'fixed',
- newPrice: parseFloat(row.competitorPrice) * 0.95,
- formula: `竞品价格 × 0.95 = $${(parseFloat(row.competitorPrice) * 0.95).toFixed(2)}`,
- effectiveTime: ''
- };
- adjustVisible.value = true;
- };
- const submitAdjust = () => {
- ElMessage.success('价格调整已提交');
- adjustVisible.value = false;
- };
- const muteAlert = (row: PriceWatchItem) => {
- ElMessage.success('已忽略该预警');
- };
- const doExport = () => {
- ElMessage.info('导出开始,完成后将自动下载');
- };
- onMounted(loadData);
- </script>
- <style scoped>
- .filter-form :deep(.el-form-item) { margin-bottom: 0; }
- </style>
|