feat 更新版本
All checks were successful
Build image / build (push) Successful in 35s

This commit is contained in:
timerzz 2024-09-13 22:10:26 +08:00
parent cd6ef161b9
commit be76d0b434
17 changed files with 1054 additions and 127 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -12,10 +12,12 @@
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "4.x", "ant-design-vue": "4.x",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.5.1",
"mande": "^2.0.8", "mande": "^2.0.8",
"moment": "^2.30.1", "moment": "^2.30.1",
"radash": "^12.1.0", "radash": "^12.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-echarts": "^7.0.3",
"vue-router": "4" "vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
@ -23,6 +25,7 @@
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"unocss": "^0.59.0", "unocss": "^0.59.0",
"unplugin-vue-components": "^0.26.0", "unplugin-vue-components": "^0.26.0",
"unplugin-vue-router": "^0.10.7",
"vite": "^5.2.0" "vite": "^5.2.0"
} }
} }

36
src/api/articles.js Normal file
View File

@ -0,0 +1,36 @@
import {mande} from "mande";
import {queryRemoveZero} from "@/api/utils.js";
const articles = mande('/api/v2/articles')
export const ListArticles = (q) => {
const query = queryRemoveZero(q)
return articles.get({query})
}
export const GetArticle = (id) =>{
return articles.get(`/${id}`)
}
export const UpdateArticle = (article)=>{
return articles.patch(article)
}
export const GetBrandsDict=()=>{
return articles.get('dict/brand')
}
export const GetProviderHistory=(id)=>{
return articles.get(`/provider/history/${id}`)
}
export const GetSellerHistory=(id)=>{
return articles.get(`/seller/history/${id}`)
}
export const UpdateProviderArticle=(article)=>{
return articles.patch('provider',article)
}
export const UpdateSellerArticle=(article)=>{
return articles.patch('seller',article)
}

View File

@ -1,25 +1,47 @@
import {mande} from "mande"; import {mande} from "mande";
import {queryRemoveZero} from "@/api/utils.js"; import {queryRemoveZero} from "@/api/utils.js";
const product = mande('/api/v2/providers') const providers = mande('/api/v2/providers')
export const ListProviders = (q) => { export const ListProviders = (q) => {
const query = queryRemoveZero(q) const query = queryRemoveZero(q)
return product.get({query}) return providers.get({query})
} }
export const CreateProvider = (provider)=>{ export const CreateProvider = (provider)=>{
return product.post(provider) return providers.post(provider)
} }
export const UpdateProvider = (provider)=>{ export const UpdateProvider = (provider)=>{
return product.put(provider) return providers.put(provider)
} }
export const GetProvider = (id) =>{ export const GetProvider = (id) =>{
return product.get(`/${id}`) return providers.get(`/${id}`)
} }
export const DeleteProvider = (id)=>{ export const DeleteProvider = (id)=>{
return product.delete(`/${id}`) return providers.delete(`/${id}`)
}
export const GetProviderDictStatus = () =>{
return providers.get('/dict/status')
}
export const FindProviders= (q) =>{
const query = queryRemoveZero(q)
return providers.get('find',{query})
}
const provider = mande('/api/v2/provider')
// 拉取供应商所有商品价格
export const FetchProviderArticles = (providerId)=>{
return provider.post(`/${providerId}/pull`)
}
export const FetchProviderArticle = (providerArticle)=>{
return provider.post(`/${providerArticle.providerId}/fetch/${providerArticle.id}`)
} }

View File

@ -1,25 +1,48 @@
import {mande} from "mande"; import {mande} from "mande";
import {queryRemoveZero} from "@/api/utils.js"; import {queryRemoveZero} from "@/api/utils.js";
const product = mande('/api/v2/sellers') const sellers = mande('/api/v2/sellers')
export const ListSellers = (q) => { export const ListSellers = (q) => {
const query = queryRemoveZero(q) const query = queryRemoveZero(q)
return product.get({query}) return sellers.get({query})
}
export const FindSellers= (q) =>{
const query = queryRemoveZero(q)
return sellers.get('find',{query})
} }
export const CreateSeller = (seller)=>{ export const CreateSeller = (seller)=>{
return product.post(seller) return sellers.post(seller)
} }
export const UpdateSeller = (seller)=>{ export const UpdateSeller = (seller)=>{
return product.put(seller) return sellers.put(seller)
} }
export const GetSeller = (id) =>{ export const GetSeller = (id) =>{
return product.get(`/${id}`) return sellers.get(`/${id}`)
} }
export const DeleteSeller = (id)=>{ export const DeleteSeller = (id)=>{
return product.delete(`/${id}`) return sellers.delete(`/${id}`)
}
export const GetSellerDictStatus = () =>{
return sellers.get('/dict/status')
}
const seller = mande('/api/v2/seller')
export const FetchSellerArticles = (sellerId)=>{
return seller.post(`/${sellerId}/pull`)
}
export const MatchSellerSku = (sellerArticle)=>{
return seller.post(`/${sellerArticle.sellerId}/match`, sellerArticle)
}
export const FetchSellerArticle = (sellerArticle)=>{
return seller.post(`/${sellerArticle.sellerId}/fetch/${sellerArticle.id}`)
} }

View File

@ -0,0 +1,170 @@
<template>
<a-collapse-panel :key="`${provider.providerId}`" >
<template #extra>
<div class="flex space-x-4">
<div>更新时间{{dayjs(provider.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}</div>
<div v-if="provider.skuID">抓取<a-switch :checked="!provider.exclude" @click="onClickExclude"/></div>
</div>
</template>
<template #header>
<span class="text-lg">{{header}}</span>
</template>
<div class="flex flex-col space-y-8">
<div class="flex space-x-4 ">
<div class="w-50">货号{{provider.pid || '-'}}</div>
<!-- <div class="w-50">SpuID: {{provider.spuID || '-'}} </div>-->
<div class="w-50">SkuID: {{provider.skuID || '-'}}</div>
</div>
<div class="flex space-x-4 items-center">
<div class="w-50">当前售价$ {{provider.cost.originalPrice || '-'}}</div>
<a-popover v-if="provider.cost.calMark" trigger="hover">
<template #content>
<span class="whitespace-pre">
{{provider.cost.calMark}}
</span>
</template>
<div class="w-50">当前到手价 {{provider.cost.finalPrice || '-'}}</div>
</a-popover>
<div class="w-50" v-else>当前到手价 {{provider.cost.finalPrice || '-'}}</div>
<a-button type="primary" @click="fetchArticle(provider)" :loading="fetchLoading">立即拉取</a-button>
</div>
<v-chart class="h-400px w-full" :option="options" />
</div>
</a-collapse-panel>
</template>
<script setup>
import dayjs from "dayjs";
import {GetProviderHistory, UpdateProviderArticle} from "@/api/articles.js";
import {message} from "ant-design-vue";
import {computed, ref, watch} from "vue";
import VChart from "vue-echarts";
const props = defineProps({
provider: {
type: Object,
default: () => {
return {
providerId: '',
updatedAt: 0,
skuID:'',
pid: '',
cost: {
originalPrice: 0,
finalPrice: 0,
calMark: ''
}
}
}
},
providerDict:{
type: Array,
default: () => {
return []
}
},
open: {
type: Boolean,
default: false
}
})
const header = computed(() => {
return props.providerDict.find(s => s.providerId === props.provider.providerId)?.name
})
const onClickExclude = (checked, e) => {
props.provider.exclude = !checked
UpdateProviderArticle(props.provider).then(res => {
if (res.code !== 200) {
message.error(`更新失败:${res.msg}`)
} else {
message.success('更新成功')
}
}).catch(err => {
message.error('更新失败')
console.log(err)
})
}
watch(() => props.open, (newVal) => {
if (newVal && !(props.provider.historyPrice?.length > 0)) {
GetProviderHistory(props.provider.id).then(res => {
if (res.code !== 200) {
message.error(`获取历史数据失败:${res.msg}`)
} else {
props.provider.historyPrice = res.data
}
})
}
})
//
import { use } from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import {FetchProviderArticle} from "@/api/provider.js";
use([GridComponent, LineChart, CanvasRenderer, TitleComponent , TooltipComponent, LegendComponent ])
const options = computed(()=> {
return {
title: {
text: "历史价格",
left: "center"
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}'
},
legend: {
left: 'left'
},
xAxis: {
type: 'category',
name: '日期',
data: (props.provider.historyPrice|| []).map(p => dayjs(p.createdAt).format('YYYY-MM-DD'))
},
yAxis: {
name: '价格'
},
series: [
{
type: 'line',
name: '原价',
data: (props.provider.historyPrice|| []).map(p => p.originalPrice)
},
{
type: 'line',
name: '到手价',
data: (props.provider.historyPrice|| []).map(p => p.finalPrice)
}
]
}
})
const fetchLoading = ref(false)
//
const fetchArticle = (pArticle) => {
fetchLoading.value = true
FetchProviderArticle(pArticle).then(res=>{
if(res.code !== 200){
message.error(`拉取失败:${res.msg}`)
}else {
message.success('拉取成功')
}
}).catch(err => {
message.error('拉取失败')
console.log(err)
}).finally(()=>{
fetchLoading.value = false
})
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,197 @@
<template>
<a-collapse-panel :key="`${seller.sellerId}`" >
<template #extra>
<div class="flex space-x-4">
<div>更新时间{{dayjs(seller.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}</div>
<div v-if="seller.skuID">抓取<a-switch :checked="!seller.exclude" @click="onClickExclude(seller)"/></div>
</div>
</template>
<template #header>
<span>{{header}}</span>
</template>
<div class="flex flex-col space-y-4">
<div class="flex space-x-4 justify-between">
<div class="w-50">货号{{seller.pid || '-'}}</div>
<div class="w-50">SpuID: {{seller.spuID || '-'}} </div>
<div class="w-100 flex items-center space-x-4">
<div>SkuID: {{seller.skuID || '-'}} </div>
<div class="text-red-4 flex items-center space-x-4" v-if="!seller.skuID">
<div>(未匹配到销售商sku)</div>
<a-button @click="matchSkuID(seller)" :disabled="loading">重新匹配</a-button>
</div>
</div>
</div>
<div class="flex space-x-4 justify-between">
<div class="w-50">当前售价 {{seller.sell.originalPrice || '-'}}</div>
<a-popover v-if="seller.sell.calMark" trigger="hover">
<template #content>
<span class="whitespace-pre">
{{seller.sell.calMark}}
</span>
</template>
<div class="w-50">当前到手价 {{seller.sell.finalPrice || '-'}}</div>
</a-popover>
<div class="w-50" v-else>当前到手价 {{seller.sell.finalPrice || '-'}}</div>
<a-button v-if="seller.skuID" type="primary" @click="fetchArticle(seller)" :loading="fetchLoading">立即拉取</a-button>
</div>
<v-chart class="h-400px w-full" :option="options" />
</div>
</a-collapse-panel>
</template>
<script setup>
import dayjs from "dayjs";
import {GetSellerHistory, UpdateSellerArticle} from "@/api/articles.js";
import {message} from "ant-design-vue";
import {computed, ref, watch} from "vue";
const props = defineProps({
seller: {
type: Object,
default: () => {
return {
sellerId: '',
updatedAt: 0,
spuID: '',
skuID:'',
pid: '',
sell: {
originalPrice: 0,
finalPrice: 0,
calMark: ''
}
}
}
},
sellerDict:{
type: Array,
default: () => {
return []
}
},
open: {
type: Boolean,
default: false
}
})
const header = computed(()=>{
return props.sellerDict.find(s => s.sellerId === props.seller.sellerId)?.name
})
const onClickExclude = (sellerArticle)=> {
sellerArticle.exclude = !sellerArticle.exclude
UpdateSellerArticle(sellerArticle).then(res=>{
if(res.code !== 200){
message.error(`更新失败:${res.msg}`)
}else {
message.success('更新成功')
}
}).catch(err => {
message.error('更新失败')
console.log(err)
})
}
watch(()=>props.open, (newVal)=> {
if(newVal && !(props.seller.historyPrice?.length > 0)){
GetSellerHistory(props.seller.id).then(res=>{
if(res.code !== 200){
message.error(`获取历史数据失败:${res.msg}`)
}else {
props.seller.historyPrice = res.data
}
})
}
})
//
import { use } from 'echarts/core'
import { LineChart } from 'echarts/charts'
import { GridComponent, TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import VChart from "vue-echarts";
import {MatchSellerSku} from "@/api/seller.js";
import {FetchSellerArticle} from "@/api/seller.js";
use([GridComponent, LineChart, CanvasRenderer, TitleComponent , TooltipComponent, LegendComponent ])
const options = computed(()=> {
return {
title: {
text: "历史价格",
left: "center"
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c}'
},
legend: {
left: 'left'
},
xAxis: {
type: 'category',
name: '日期',
data: (props.seller.historyPrice|| []).map(p => dayjs(p.createdAt).format('YYYY-MM-DD'))
},
yAxis: {
name: '价格'
},
series: [
{
type: 'line',
name: '原售价',
data: (props.seller.historyPrice|| []).map(p => p.originalPrice)
},
{
type: 'line',
name: '到手价',
data: (props.seller.historyPrice|| []).map(p => p.finalPrice)
}
]
}
})
const loading = ref(false)
// sku
const matchSkuID = (sellerArticle)=>{
loading.value = true
MatchSellerSku(sellerArticle).then(res=>{
if(res.code !== 200){
message.error(`匹配失败:${res.msg}`)
}else {
message.success('匹配成功')
}
}).catch(err => {
message.error('匹配失败')
console.log(err)
}).finally(()=>{
loading.value = false
})
}
const fetchLoading = ref(false)
//
const fetchArticle = (sArticle) => {
fetchLoading.value = true
FetchSellerArticle(sArticle).then(res=>{
if(res.code !== 200){
message.error(`拉取失败:${res.msg}`)
}else {
message.success('拉取成功')
}
}).catch(err => {
message.error('拉取失败')
console.log(err)
}).finally(()=>{
fetchLoading.value = false
})
}
</script>
<style scoped>
</style>

View File

@ -1,50 +1,15 @@
import {createRouter, createWebHashHistory} from "vue-router"; import {createRouter, createWebHashHistory} from "vue-router";
import { routes } from 'vue-router/auto-routes';
const routes = [
{
path: '/',
redirect: '/watcher'
},
{
path: '/watcher',
name: 'watcher',
component: ()=>import('@/views/Watcher/index.vue')
},
{
path: '/us-coach',
name: 'us-coach',
component: ()=>import('@/views/Product/USCoachOutlet.vue')
},
{
path: '/cn-coach',
name: 'cn-coach',
component: ()=>import('@/views/Product/CNCoachOutlet.vue')
},
{
path: '/cn-coach-outlet',
name: 'cn-coach-outlet',
component: ()=>import('@/views/Product/CNCoachOutlet.vue')
},
{
path: '/pusher',
name: 'pusher',
component: ()=>import('@/views/Pusher/index.vue')
},
{
path: '/provider',
name: 'provider',
component: ()=>import('@/views/Provider/index.vue')
},
{
path: '/seller',
name: 'seller',
component: ()=>import('@/views/Seller/index.vue')
}
]
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes routes:[
...routes,
{ path: '/',
redirect: '/watcher'
},
]
}) })
export default router export default router

View File

@ -0,0 +1,180 @@
<template>
<div class="overflow-overlay h-full m-4 bg-white rounded-2 shadow-lg p-8 flex flex-col justify-between space-y-4">
<div class="border-solid border-2 border-gray-100 rounded-lg p-4 flex space-x-8">
<a-image :src="data.image" :width="200"></a-image>
<div class="flex justify-between w-full pr-16">
<div class="p-4 flex flex-col justify-between">
<div class="text-xl font-bold">名称{{data.name}}</div>
<div class="text-xl font-bold">品牌{{data.brand}}</div>
<div class="text-xl font-bold">货号{{data.pid}}</div>
</div>
<div class="p-4 flex flex-col space-y-2 justify-between">
<div class="flex space-x-12">
<div class="text-xl font-bold">成本价{{data.costPrice}}</div>
<div class="text-xl font-bold">售价{{data.sellPrice}}</div>
</div>
<div class="text-xl font-bold">收益率{{data.rate}}%</div>
<div class="flex space-x-4 items-center">
<span class="text-xl font-bold mr-4 whitespace-nowrap">备注:</span>
<span v-if="!remarkEdit">{{data.remark || '-'}}</span>
<EditOutlined v-if="!remarkEdit" class="cursor-pointer" @click="remarkEdit=true" />
<a-input v-else v-model:value="data.remark"></a-input>
<SaveOutlined v-show="remarkEdit" class="cursor-pointer" @click="onSaveRemark"/>
</div>
</div>
<div class="p-4 flex flex-col space-y-2 justify-between">
<div class="text-xl font-bold">更新时间{{dayjs(data.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}</div>
<div class="text-xl font-bold">可购买
<CheckOutlined v-if="data.available" class="text-green-600"/>
<CloseOutlined v-else class="text-red-7"/>
</div>
<div class="text-xl font-bold">
抓取<a-switch :checked="!data.exclude" @click="onClickExclude(data)"/>
</div>
</div>
</div>
</div>
<div class="h-full overflow-overlay">
<a-card
class="h-full flex flex-col w-full"
:bodyStyle="{height: '100%', flex: '1 1 0%', overflow: 'overlay'}"
:tab-list="[{key:'providers',tab:'供应商'},{key:'sellers',tab:'销售商'}]"
v-model:active-tab-key="activeTabKey"
@tab-change="onTabChange"
>
<div>
<a-collapse v-model:activeKey="openTabKey" >
<seller-article-panel v-if="activeTabKey === 'sellers'" v-for="item in data.sellers || []" :key="`${item.sellerId}`"
:seller="item" :seller-dict="dict.sellers" :open="openTabKey.indexOf(item.sellerId) > -1">
</seller-article-panel>
<provider-article-panel v-else-if="activeTabKey === 'providers'" v-for="item in data.providers || []" :key="`${item.providerId}`"
:provider="item" :provider-dict="dict.providers" :open="openTabKey.indexOf(item.providerId) > -1">
</provider-article-panel>
</a-collapse>
</div>
</a-card>
</div>
</div>
</template>
<script setup>
import {onMounted, reactive, ref} from "vue";
import {useRoute} from "vue-router";
import {GetArticle, GetBrandsDict, UpdateArticle} from "@/api/articles.js";
import {message} from "ant-design-vue";
import dayjs from "dayjs";
import {CheckOutlined, CloseOutlined, EditOutlined, SaveOutlined} from "@ant-design/icons-vue";
import {FindSellers} from "@/api/seller.js";
import {FindProviders} from "@/api/provider.js";
import SellerArticlePanel from "@/componse/seller-article/seller-article-panel.vue";
import ProviderArticlePanel from "@/componse/provider-article/provider-article-panel.vue";
const loading = ref(false)
const data = ref({
image:'',
name:'',
providers: [],
sellers: [],
remark:''
})
onMounted(()=>{
load()
loadDict()
})
const route = useRoute()
const load = ()=>{
loading.value = true
GetArticle(route.query.id).then(res=>{
if(res.code === 200){
data.value = res.data
}else{
message.error(res.msg)
}
}).catch((error)=>{
message.error('获取详情失败')
console.log(error)
}).finally(()=>{
loading.value = false
})
}
const onClickExclude=(record)=>{
record.exclude = !record.exclude
UpdateArticle(record).then(res=>{
if (res.code !== 200) {
message.error(`${record.exclude ? '关闭':'开启'}失败`)
}else {
message.success(`${record.exclude ? '关闭':'开启'}成功`)
}
}).catch(err=>{
message.error(`${record.exclude ? '关闭':'开启'}失败`)
console.log(err)
}).finally(()=>{
load()
})
}
//
const dict = reactive({
brands: [],
providers: [],
sellers: []
})
const loadDict = ()=>{
Promise.all([FindSellers({}), FindProviders({}), GetBrandsDict()]).then(([sellers, providers, brands])=>{
if (sellers.code === 200 && providers.code === 200 && brands.code === 200) {
dict.sellers = sellers.data
dict.providers = providers.data
dict.brands = brands.data
}else if(sellers.code !== 200){
message.error('获取销售商列表失败')
}else if(providers.code !== 200){
message.error('获取供应商列表失败')
}else if(brands.code !== 200){
message.error('获取品牌列表失败')
}
}).catch(err => {
message.error('获取字典信息失败')
console.log(err)
})
}
const activeTabKey = ref('providers')
const onTabChange = (key)=>{
activeTabKey.value = key
}
const openTabKey = ref([])
//
const remarkEdit = ref(false)
const onSaveRemark = ()=>{
UpdateArticle(data.value).then(res=>{
if (res.code !== 200) {
message.error('保存失败')
}else {
message.success('保存成功')
}
}).catch(err=>{
message.error('保存失败')
console.log(err)
}).finally(()=>{
remarkEdit.value = false
})
}
</script>
<style scoped>
</style>

205
src/views/article/index.vue Normal file
View File

@ -0,0 +1,205 @@
<template>
<div class="h-full m-4 bg-white rounded-2 shadow-lg p-8 flex flex-col justify-between space-y-4">
<div class="flex justify-between">
<div></div>
<div class="flex space-x-4">
<a-input placeholder="请输入关键词" v-model:value="query.keyword"></a-input>
<a-button type="primary" :disabled="loading" @click="list">搜索</a-button>
</div>
</div>
<div class="h-full border-0 border-t-1 border-solid border-gray-300 pt-4">
<a-spin :spinning="loading" :indicator="indicator">
<a-table :dataSource="data.list" :columns="columns" :pagination="false" :scroll="{y:1000}" @change="tableChange" rowKey="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :width="75" :src="record.image"/>
</template>
<template v-else-if="column.key === 'name'">
<span class="cursor-pointer text-blue" title="查看详情" @click="toDetail(record.id)">{{record.name}}</span>
</template>
<template v-else-if="column.key === 'pid'">
<span class="cursor-pointer text-blue" title="查看详情" @click="toDetail(record.id)">{{record.pid}}</span>
</template>
<template v-else-if="column.key === 'rate'">
<span>{{record.rate > 0 ? `${record.rate}%`:'--' }}</span>
</template>
<template v-else-if="column.key === 'available'">
<span :class="[record.available ? 'text-green-700':'text-red-500']">{{record.available ? '是':'否'}}</span>
</template>
<template v-else-if="column.key === 'exclude'">
<a-switch :checked="!record.exclude" @click="onClickExclude(record)"/>
</template>
<template v-else-if="column.key === 'remark'">
{{record.remark===''?'-':record.remark}}
</template>
</template>
</a-table>
</a-spin>
</div>
<a-pagination :disabled="loading" class="text-right" v-model:current="query.page" v-model:page-size="query.size" :total="data.total" show-less-items @change="list"/>
</div>
</template>
<script setup>
import {computed, h, onMounted, reactive, ref, watch} from "vue";
import {LoadingOutlined} from "@ant-design/icons-vue";
import {message} from "ant-design-vue";
import {ListArticles, UpdateArticle} from "@/api/articles.js";
import {useRouter} from "vue-router";
const query = reactive({
keyword: "",
page: 1,
size: 8,
pid:'',
brand: '',
available: null,
rate_sort:null,
})
watch(()=>query.available, ()=>{
console.log(query)
})
onMounted(()=>{
list()
})
const loading = ref(false)
//
const data = reactive({
list: [],
total: 0
})
const list = () => {
loading.value = true
ListArticles(query).then(res => {
if (res.code !== 200) {
message.error(res.msg)
}else {
data.list = res.data.list
data.total = res.data.total
}
}).finally(()=>{
loading.value = false
})
}
const columns = computed(()=>[
{
title: '货号',
dataIndex: 'pid',
key: 'pid',
width: 200
},
{
title: '名称',
dataIndex: 'name',
ellipsis: true,
key: 'name',
width: 350
},
{
title: '图片',
key: 'image',
width: 250
},
{
title: '最低成本价',
dataIndex: 'costPrice',
key: 'costPrice',
width: 150
},
{
title: '最低售价',
dataIndex: 'sellPrice',
key: 'sellPrice',
width: 150
},
{
title: '利润率',
key: 'rate',
dataIndex: 'rate',
width: 150,
sorter:true,
sortOrder: query.rate_sort
},
{
title: '可购买',
key: 'available',
dataIndex: 'available',
width: 150,
filteredValue: query.available,
filterMultiple:false,
filters:[
{
text: '可购买',
value: true,
},
{
text: '不可购买',
value: false,
},
]
},
{
title: '需要更新',
key: 'exclude',
width: 150
},
{
title: '备注',
key: 'remark',
dataIndex: 'remark',
ellipsis: true,
}
])
const indicator = h(LoadingOutlined, {
style: {
fontSize: '32px',
},
spin: true,
});
const onClickExclude=(record)=>{
record.exclude = !record.exclude
UpdateArticle(record).then(res=>{
if (res.code !== 200) {
message.error(`${record.exclude ? '关闭':'开启'}失败`)
}else {
message.success(`${record.exclude ? '关闭':'开启'}成功`)
}
}).catch(err=>{
message.error(`${record.exclude ? '关闭':'开启'}失败`)
console.log(err)
})
}
const router = useRouter()
//
const toDetail=(id)=>{
router.push({
path: '/article/detail',
query: {
id: id
}
})
}
//
const tableChange = (pagination, filters, sorter, { action, currentDataSource })=>{
if(sorter.columnKey === 'rate') {
query.rate_sort = sorter.order
}
query.available = filters.available
list()
}
</script>
<style scoped>
</style>

View File

@ -16,7 +16,7 @@ const router = useRouter()
const onclick = ({key}) => { const onclick = ({key}) => {
router.push({ router.push({
name: key path: '/'+key
}) })
} }
@ -28,28 +28,11 @@ const items = [
title: '蹲货', title: '蹲货',
}, },
{ {
key: 'product', key: 'article',
icon: () => h(DollarCircleOutlined), icon: () => h(DollarCircleOutlined),
label: '商品', label: '商品',
title: '商品', title: '商品',
onTitleClick: onclick, onTitleClick: onclick,
children: [
{
key: 'us-coach',
label: '美国coach outlet',
title: '美国coach outlet',
},
{
key: 'cn-coach-outlet',
label: '中国coach outlet',
title: '中国coach outlet',
},
{
key: 'cn-coach',
label: '中国coach',
title: '中国coach',
}
]
}, },
{ {
key: 'pusher', key: 'pusher',
@ -66,8 +49,8 @@ const items = [
{ {
key: 'seller', key: 'seller',
icon: () => h(DeploymentUnitOutlined), icon: () => h(DeploymentUnitOutlined),
label: '出货商', label: '销售商',
title: '出货商', title: '销售商',
} }
] ]

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="h-full w-full flex flex-col"> <div class="h-full w-full flex flex-col">
<Header></Header> <Header></Header>
<div class="h-full w-full flex"> <div class="h-full w-full flex overflow-overlay">
<Aside></Aside> <Aside></Aside>
<Main></Main> <Main></Main>
</div> </div>

View File

@ -16,9 +16,16 @@
</template> </template>
<template v-else-if="column.dataIndex === 'name'"> <template v-else-if="column.dataIndex === 'name'">
</template> </template>
<template v-else-if="column.key === 'ticker'">
<span>{{record.config.ticker}}</span>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusDict(record.status).color">{{getStatusDict(record.status).title}}</a-tag>
</template>
<template v-else-if="column.key === 'opt'"> <template v-else-if="column.key === 'opt'">
<a-button type="link" @click="onClickUpdate(record.id)">编辑</a-button> <a-button type="link" :disabled="['provider_status_normal', 'provider_status_error'].indexOf(getStatusDict(record.status).key) < 0" @click="onClickPull(record)">拉取</a-button>
<a-button type="link" danger @click="onClickDelete(record.id)"> 删除</a-button> <a-button type="link" :disabled="['provider_status_normal', 'provider_status_error'].indexOf(getStatusDict(record.status).key) < 0" @click="onClickUpdate(record.id)">编辑</a-button>
<a-button type="link" :disabled="['provider_status_normal', 'provider_status_error'].indexOf(getStatusDict(record.status).key) < 0" danger @click="onClickDelete(record.id)"> 删除</a-button>
</template> </template>
</template> </template>
</a-table> </a-table>
@ -76,10 +83,18 @@
<script setup> <script setup>
import {h, onMounted, reactive, ref} from "vue"; import {createVNode, h, onMounted, reactive, ref} from "vue";
import {CreateProvider, DeleteProvider, GetProvider, ListProviders, UpdateProvider} from "@/api/provider.js"; import {
import {message} from "ant-design-vue"; CreateProvider,
import {LoadingOutlined} from "@ant-design/icons-vue"; DeleteProvider,
GetProvider,
ListProviders,
FetchProviderArticles,
UpdateProvider,
GetProviderDictStatus
} from "@/api/provider.js";
import {message, Modal} from "ant-design-vue";
import {ExclamationCircleOutlined, LoadingOutlined} from "@ant-design/icons-vue";
const query = reactive({ const query = reactive({
@ -90,6 +105,11 @@ const query = reactive({
id: '', id: '',
}) })
//
const configs = reactive({
status: []
})
const loading = ref(false) const loading = ref(false)
// //
const data = reactive({ const data = reactive({
@ -111,10 +131,29 @@ const list = () => {
}) })
} }
const loadStatus = () => {
GetProviderDictStatus().then(res=>{
if (res.code !== 200) {
message.error(res.msg)
}else {
configs.status = res.data
}
})
}
const getStatusDict = (value)=>{
return configs.status.find(s => s.value === value) || {
title: '未知',
color: ''
}
}
onMounted(()=>{ onMounted(()=>{
list() list()
loadStatus()
}) })
//********************//
const addModal = reactive({ const addModal = reactive({
visible: false, visible: false,
data: { data: {
@ -197,6 +236,17 @@ const columns = [
key: 'name', key: 'name',
width: 250 width: 250
}, },
{
title: '抓取时间',
key: 'ticker',
width: 250
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 250
},
{ {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',
@ -241,6 +291,29 @@ const onClickDelete=(id)=>{
list() list()
}) })
} }
const onClickPull = (record)=>{
Modal.confirm({
title: '提示',
icon: createVNode(ExclamationCircleOutlined),
content: `确认拉取${record.name}商品价格?`,
onOk() {
FetchProviderArticles(record.providerId).then(res=>{
if (res.code !== 200) {
message.error(res.msg)
}else {
message.success("开始抓取")
}
}).catch(err =>{
console.log(err)
message.error("抓取失败")
}).finally(()=>{
list()
})
},
centered: true
});
}
</script> </script>
<style scoped> <style scoped>

View File

@ -16,9 +16,16 @@
</template> </template>
<template v-else-if="column.dataIndex === 'name'"> <template v-else-if="column.dataIndex === 'name'">
</template> </template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusDict(record.status).color">{{getStatusDict(record.status).title}}</a-tag>
</template>
<template v-else-if="column.key === 'ticker'">
<span>{{record.config.ticker}}</span>
</template>
<template v-else-if="column.key === 'opt'"> <template v-else-if="column.key === 'opt'">
<a-button type="link" @click="onClickUpdate(record.id)">编辑</a-button> <a-button type="link" :disabled="['seller_status_normal', 'seller_status_error'].indexOf(getStatusDict(record.status).key) < 0" @click="onClickPull(record)">拉取</a-button>
<a-button type="link" danger @click="onClickDelete(record.id)"> 删除</a-button> <a-button type="link" :disabled="['seller_status_normal', 'seller_status_error'].indexOf(getStatusDict(record.status).key) < 0" @click="onClickUpdate(record.id)">编辑</a-button>
<a-button type="link" :disabled="['seller_status_normal', 'seller_status_error'].indexOf(getStatusDict(record.status).key) < 0" danger @click="onClickDelete(record.id)"> 删除</a-button>
</template> </template>
</template> </template>
</a-table> </a-table>
@ -26,7 +33,7 @@
</div> </div>
<a-pagination :disabled="loading" class="text-right" v-model:current="query.page" :total="data.total" show-less-items @change="list"/> <a-pagination :disabled="loading" class="text-right" v-model:current="query.page" :total="data.total" show-less-items @change="list"/>
</div> </div>
<a-modal v-model:open="addModal.visible" :title="`${addModal.edit ? '编辑' : '添加'}出货商`" @ok="handleOk" centered width="80%"> <a-modal v-model:open="addModal.visible" :title="`${addModal.edit ? '编辑' : '添加'}销售商`" @ok="handleOk" centered width="80%">
<a-spin :spinning="addModal.loading" :indicator="indicator"> <a-spin :spinning="addModal.loading" :indicator="indicator">
<a-form <a-form
ref="formRef" ref="formRef"
@ -37,10 +44,10 @@
@finish="cleanAddModal" @finish="cleanAddModal"
@finishFailed="cleanAddModal" @finishFailed="cleanAddModal"
> >
<a-form-item label="标识" name="sellerId" :rules="[{ required: true, message: '请填写出货商标识' }]"> <a-form-item label="标识" name="sellerId" :rules="[{ required: true, message: '请填写销售商标识' }]">
<a-input v-model:value="addModal.data.sellerId" placeholder="请输入标识比如dewu" :disabled="addModal.edit"/> <a-input v-model:value="addModal.data.sellerId" placeholder="请输入标识比如dewu" :disabled="addModal.edit"/>
</a-form-item> </a-form-item>
<a-form-item label="名称" name="name" :rules="[{ required: true, message: '请填写出货商名称' }]"> <a-form-item label="名称" name="name" :rules="[{ required: true, message: '请填写销售商名称' }]">
<a-input v-model:value="addModal.data.name" placeholder="请输入名称,比如:得物"/> <a-input v-model:value="addModal.data.name" placeholder="请输入名称,比如:得物"/>
</a-form-item> </a-form-item>
<a-form-item label="抓取定时" name="config.ticker"> <a-form-item label="抓取定时" name="config.ticker">
@ -70,10 +77,18 @@
<script setup> <script setup>
import {h, onMounted, reactive, ref} from "vue"; import {createVNode, h, onMounted, reactive, ref} from "vue";
import {CreateSeller, DeleteSeller, GetSeller, ListSellers, UpdateSeller} from "@/api/seller.js"; import {
import {message} from "ant-design-vue"; CreateSeller,
import {LoadingOutlined} from "@ant-design/icons-vue"; DeleteSeller,
GetSeller,
ListSellers,
UpdateSeller,
FetchSellerArticles,
GetSellerDictStatus
} from "@/api/seller.js";
import {message, Modal} from "ant-design-vue";
import {ExclamationCircleOutlined, LoadingOutlined} from "@ant-design/icons-vue";
const query = reactive({ const query = reactive({
@ -84,6 +99,11 @@ const query = reactive({
id: '', id: '',
}) })
//
const configs = reactive({
status: []
})
const loading = ref(false) const loading = ref(false)
// //
const data = reactive({ const data = reactive({
@ -105,8 +125,26 @@ const list = () => {
}) })
} }
const loadStatus = () => {
GetSellerDictStatus().then(res=>{
if (res.code !== 200) {
message.error(res.msg)
}else {
configs.status = res.data
}
})
}
const getStatusDict = (value)=>{
return configs.status.find(s => s.value === value) || {
title: '未知',
color: ''
}
}
onMounted(()=>{ onMounted(()=>{
list() list()
loadStatus()
}) })
const addModal = reactive({ const addModal = reactive({
@ -187,6 +225,17 @@ const columns = [
key: 'name', key: 'name',
width: 250 width: 250
}, },
{
title: '抓取时间',
key: 'ticker',
width: 250
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 250
},
{ {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',
@ -231,6 +280,29 @@ const onClickDelete=(id)=>{
list() list()
}) })
} }
const onClickPull = (record)=>{
Modal.confirm({
title: '提示',
icon: createVNode(ExclamationCircleOutlined),
content: `确认拉取${record.name}商品价格?`,
onOk() {
FetchSellerArticles(record.sellerId).then(res=>{
if (res.code !== 200) {
message.error(res.msg)
}else {
message.success("开始抓取")
}
}).catch(err =>{
console.log(err)
message.error("抓取失败")
}).finally(()=>{
list()
})
},
centered: true
});
}
</script> </script>
<style scoped> <style scoped>

View File

@ -4,12 +4,17 @@ import UnoCSS from 'unocss/vite'
import path from 'path' import path from 'path'
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import VueRouter from 'unplugin-vue-router/vite';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
VueRouter({
routesFolder: 'src/views'
}),
UnoCSS(), UnoCSS(),
Components({ Components({
resolvers: [ resolvers: [
@ -22,41 +27,6 @@ export default defineConfig({
server:{ server:{
open: true, open: true,
proxy: { proxy: {
// '/storage/v1/products': {
// // target: 'https://ht.timerzz.com:20443/',
// target: 'http://192.168.31.163:31828/',
// changeOrigin: true,
// secure: false,
// ws: true,
// },
// '/storage/v1/push': {
// target: 'https://ht-dev.timerzz.com:20443/',
// // target: 'http://192.168.31.55:2280/',
// changeOrigin: true,
// secure: false,
// ws: true,
// },
// '/storage/v1/pushers': {
// target: 'https://ht-dev.timerzz.com:20443/',
// // target: 'http://192.168.31.55:2280/',
// changeOrigin: true,
// secure: false,
// ws: true,
// },
// '/storage/v1/watchers': {
// target: 'https://ht-dev.timerzz.com:20443/',
// // target: 'http://192.168.31.55:2280/',
// changeOrigin: true,
// secure: false,
// ws: true,
// },
// '/storage/v1/proxies': {
// target: 'https://ht-dev.timerzz.com:20443/',
// // target: 'http://192.168.31.55:2280/',
// changeOrigin: true,
// secure: false,
// ws: true,
// },
'/api/v1': { '/api/v1': {
target: 'https://ht.timerzz.com:20443/', target: 'https://ht.timerzz.com:20443/',
// target: 'http://192.168.31.55:2280/', // target: 'http://192.168.31.55:2280/',
@ -64,12 +34,40 @@ export default defineConfig({
secure: false, secure: false,
ws: true, ws: true,
}, },
'/api/v2': { '/api/v2/sellers': {
target: 'http://localhost:8081/', target: 'http://localhost:8081/',
// target: 'http://192.168.31.55:2280/', // target: 'http://192.168.31.55:2280/',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
},
'/api/v2/seller': {
target: 'http://localhost:8083/',
// target: 'http://192.168.31.55:2280/',
changeOrigin: true,
secure: false,
ws: true,
},
'/api/v2/providers': {
target: 'http://localhost:8080/',
// target: 'http://192.168.31.55:2280/',
changeOrigin: true,
secure: false,
ws: true,
},
'/api/v2/provider': {
target: 'http://172.21.195.130:8082/',
// target: 'http://192.168.31.55:2280/',
changeOrigin: true,
secure: false,
ws: true,
},
'/api/v2/articles': {
target: 'http://localhost:8085/',
// target: 'http://192.168.31.55:2280/',
changeOrigin: true,
secure: false,
ws: true,
} }
}, },
}, },