Skip to content

Commit

Permalink
[新功能] 跑商路线规划(路线单排收益) (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ximu-Luya authored Mar 24, 2024
2 parents 5c14147 + 83dceea commit 5d09aa2
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 42 deletions.
4 changes: 4 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<script setup lang="ts">
import '@/utils/info';
import { Toaster } from '@/components/ui/sonner';
const logStore = useLatestLogs();
await logStore.startGetData();
</script>

<template>
Expand Down
15 changes: 5 additions & 10 deletions components/City.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,14 @@ const sortCitesByProfit = (
!latestLog
|| Date.now() - new Date(latestLog.uploadedAt).valueOf() > 1 * 24 * 60 * 60 * 1000
|| !sourceCityPrice
) return { cityName: city.name, profit: -9999 };
// 如果最新交易记录无效,排名在有效记录之后,且按顺序排列
else if (isLogValid(latestLog))
) return { cityName: city.name, profit: -99999 };
else {
const profit = settingStore.getProfitWithRule(latestLog.price, sourceCityPrice);
return {
cityName: city.name,
profit: Math.round(latestLog.price * 1.2 * 0.98 - sourceCityPrice * 0.8 * 1.08) - 9000
};
// 如果最新交易记录有效,按利润高低排名
else
return {
cityName: city.name,
profit: Math.round(latestLog.price * 1.2 * 0.98 - sourceCityPrice * 0.8 * 1.08)
profit: profit - (isLogValid(latestLog) ? 0 : 9999) // 如果最新交易记录无效,排名在有效记录之后,且按顺序排列
};
}
})
.forEach((cityProfit) => (citiesProfitMap[cityProfit.cityName] = cityProfit.profit));
Expand Down
106 changes: 106 additions & 0 deletions components/PerTicketProfitRouteTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script setup lang="ts">
const logStore = useLatestLogs();
const settingStore = useSettingStore();
const props = defineProps<{ isComeAndGo: boolean }>();
type TransactionRoute = {
originatingCity: string;
destinationCity: string;
};
// 跑商路线
const transactionRoute = computed(() => {
const oneWayRoute: TransactionRoute[] = [];
cities.forEach(originatingCity => {
cities.forEach(destinationCity => {
if (originatingCity.name !== destinationCity.name) {
// 存在往返路线
const isExistComeAndGoRoute = oneWayRoute.some(route => {
return route.originatingCity === destinationCity.name && route.destinationCity === originatingCity.name;
});
if (props.isComeAndGo && isExistComeAndGoRoute) return;
oneWayRoute.push({
originatingCity: originatingCity.name,
destinationCity: destinationCity.name
});
}
});
});
return oneWayRoute;
});
const sortedTransactionRoute = computed(() => {
// 计算每条路线的单票利润
const transactionRouteWithProfit = transactionRoute.value.map(route => {
return {
...route,
profit: getCityProfit(route.originatingCity, route.destinationCity)
+ (props.isComeAndGo ? getCityProfit(route.destinationCity, route.originatingCity) : 0)
};
});
// 按利润排序
return transactionRouteWithProfit.toSorted((a, b) => b.profit - a.profit);
});
const rankBarWidthPercent = (profit: number) => {
const maxProfit = sortedTransactionRoute.value[0].profit;
const minProfit = sortedTransactionRoute.value[sortedTransactionRoute.value.length - 1].profit;
return ((profit - minProfit) / (maxProfit - minProfit)) * 90;
};
const getCityProfit = (originatingCity: string, destinationCity: string) => {
const allProduct = cities.find(city => city.name === originatingCity)?.products;
const cityAllProductPerTicketProfit = allProduct?.reduce((result, currentProduct) => {
// 仅计算展示的商品
if (!currentProduct.valuable) return result;
const sourceCityPrice = logStore.getLatestLog(originatingCity, currentProduct.name, originatingCity)?.price || 0;
const targetCityPrice = logStore.getLatestLog(originatingCity, currentProduct.name, destinationCity)?.price || 0;
const profit = settingStore.getProfitWithRule(sourceCityPrice, targetCityPrice);
const perTicketProfit = profit * (currentProduct.baseVolume || 0);
return result + perTicketProfit;
}, 0);
return cityAllProductPerTicketProfit || 0;
};
</script>

<template>
<Card class="w-full">
<CardContent class="pt-2">
<Table class="table-fixed">
<TableHeader>
<TableRow>
<TableHead class="border-r min-w-[160px] max-w-[240px]">跑商路线</TableHead>
<TableHead class="min-w-[100px] w-1/2">单票利润</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="route in sortedTransactionRoute"
:key="`${route.originatingCity}->${route.destinationCity}`"
>
<TableCell class="border-r">
<div class="flex flex-col sm:items-center sm:flex-row">
<span>{{ route.originatingCity }}</span>
<span v-if="props.isComeAndGo" class="i-icon-park-outline-transfer-data m-1 rotate-90 sm:rotate-0"></span>
<span v-else class="i-icon-park-outline-arrow-right m-1 rotate-90 sm:rotate-0"></span>
<span>{{ route.destinationCity }}</span>
</div>
</TableCell>
<TableCell>
<div class="w-full">
{{ route.profit }}
<div :style="{ width: `${rankBarWidthPercent(route.profit)}%` }" class="h-2 bg-primary"></div>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</template>
4 changes: 2 additions & 2 deletions components/Price.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const reportDialogVisible = ref(false);
const isOutdated = computed(() => {
if (!props.log) return true;
props.timestamp;
return isLogValid(props.log);
return !isLogValid(props.log);
});
// 单位利润
Expand All @@ -39,7 +39,7 @@ const profit = computed(() => {
props.log.sourceCity
);
if (sourceCityLatestLog) {
return Math.round(props.log.price * 1.2 * 0.98 - sourceCityLatestLog.price * 0.8 * 1.08);
return settingStore.getProfitWithRule(props.log.price, sourceCityLatestLog.price);
} else {
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion components/ui/menubar/Menubar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<MenubarRoot
v-bind="forwarded"
:class="cn('flex h-10 items-center gap-x-1 rounded-md bg-background pb-1', props.class)"
:class="cn('flex items-center gap-x-1 rounded-md bg-background pb-1', props.class)"
>
<slot />
</MenubarRoot>
Expand Down
17 changes: 10 additions & 7 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ const settingStore = useSettingStore();
<div>
<NuxtLink to="/" class="text-2xl font-bold hover:text-base-600 select-none">雷索纳斯市场</NuxtLink>
</div>
<Menubar class="mt-4">
<Menubar class="mt-4 flex-wrap">
<MenubarButton>
<NuxtLink to="/">首页</NuxtLink>
</MenubarButton>
<MenubarButton>
<NuxtLink to="/transaction-planning">路线规划</NuxtLink>
</MenubarButton>
<MenubarMenu>
<MenubarTrigger>商品</MenubarTrigger>
<MenubarContent>
Expand Down Expand Up @@ -147,37 +150,37 @@ const settingStore = useSettingStore();
>
<div class="flex items-center mr-2">
<span class="i-icon-park-outline-percentage mr-1 block w-4"></span>
<span>买卖税收8%</span>
<span>买卖税收(8%)</span>
</div>
<span class="i-material-symbols-check"></span>
</a>
</MenubarItem>
<MenubarItem as-child>
<a
class="hover:bg-gray-100 cursor-pointer flex justify-between"
@click="toast('功能正在开发中,敬请期待')"
@click="settingStore.priceChangeRate = 0.2"
>
<div class="flex items-center mr-2">
<span class="i-icon-park-outline-positive-dynamics mr-1 block w-4"></span>
<span>最大砍价抬价</span>
<span>最大砍价抬价(20%)</span>
</div>
<span
v-if="settingStore.profitComputeRule === 'maxPriceChange'"
v-if="settingStore.priceChangeRate === 0.2"
class="i-material-symbols-check"
></span>
</a>
</MenubarItem>
<MenubarItem as-child>
<a
class="hover:bg-gray-100 cursor-pointer flex justify-between"
@click="toast('功能正在开发中,敬请期待')"
@click="settingStore.priceChangeRate = 0"
>
<div class="flex items-center mr-2">
<span class="i-icon-park-outline-negative-dynamics mr-1 block w-4"></span>
<span>不砍价不抬价</span>
</div>
<span
v-if="settingStore.profitComputeRule === 'noChange'"
v-if="settingStore.priceChangeRate === 0"
class="i-material-symbols-check"
></span>
</a>
Expand Down
4 changes: 0 additions & 4 deletions pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
<script setup lang="ts">
import { useStorage } from '@vueuse/core';
const logStore = useLatestLogs();
await logStore.startGetData();
const selectedCity = ref<CityInfo[]>(cities);
const blockCities = useStorage<string[]>('blockCities', []);
Expand Down
38 changes: 38 additions & 0 deletions pages/transaction-planning.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue';
const settingStore = useSettingStore();
const isComeAndGo = ref('往返'); // 是否往返
</script>

<template>
<div class="main pb-12">
<div class="mb-4">
<span href="#" class="block mt-1 text-lg leading-tight font-bold text-base-800">跑商路线规划</span>
<div class="mt-2 px-2 text-slate-500">
<p class="mb-2">亲爱的列车长们: </p>
<p class="ml-2">这是阿妮塔科技为您整理的两地运输路线收益排行。</p>
<p class="ml-2">我们建议选择城市售价靠前的商品(一般为城市特产品)作为主攻对象,每次使用5本以内的进货采购书将货物尽可能填满车厢,这样可以在单程收益和进货采购数使用量之间维持一定平衡。</p>
<p class="ml-2">希望这份排行能为您的运输之路提供参考。</p>
<p class="ml-2">祝各位列车长旅途顺利,财源滚滚!</p>
<p class="my-2">阿妮塔科技敬上</p>
注:各位列车长的个性化配置页面正在开发中,目前展示的收益仅考虑特产商品,计算参数为(砍价抬价:{{ settingStore.priceChangeRate * 100 }}%,城市税率:{{ settingStore.taxRate * 100 }}%,特产品交易量仅为基础值)。
</div>
</div>

<div class="flex flex-wrap gap-4 mb-4">
<Button
v-for="item in ['单程', '往返']"
:key="item"
@click="isComeAndGo = item"
:variant="isComeAndGo == item ? 'default' : 'outline'"
size="sm"
>{{ item }}</Button>
</div>

<div class="space-y-4">
<PerTicketProfitRouteTable :isComeAndGo="isComeAndGo == '往返'" />
</div>
</div>
</template>
8 changes: 5 additions & 3 deletions stores/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ export const useLatestLogs = defineStore('latest_logs', () => {
});
};

const getLatestLog = (sourceCity: string, productName: string, targetCity: string) => {
return transactionMap.value.get(`${productName}-from${sourceCity}to${targetCity}`);
};

return {
logs,
getLatestLog(sourceCity: string, productName: string, targetCity: string) {
return transactionMap.value.get(`${productName}-from${sourceCity}to${targetCity}`);
},
getLatestLog,
fetch,
startGetData
};
Expand Down
32 changes: 18 additions & 14 deletions stores/settings.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// import { umami } from '~analytics/umami';

export type ListSortMode = 'byCity' | 'byProfit';
export type ProfitComputeRule = 'maxPriceChange' | 'noChange';
export type DataDisplayItems = 'profit' | 'perTicketProfit';

export const useSettingStore = defineStore('setting', () => {
// const listSortMode = useStorage<ListSortMode>('listSortMode', 'byCity')
// const profitComputeRule = useStorage<ProfitComputeRule>('profitComputeRule', 'noChange')

const listSortMode = ref<ListSortMode>('byCity');
const dataDisplayItems = ref<DataDisplayItems[]>(['profit', 'perTicketProfit']);
const profitComputeRule = ref<ProfitComputeRule>('maxPriceChange');
const taxRate = ref<number>(0.08);
const priceChangeRate = ref<number>(0.2);

// 切换列表排序模式
const switchListSortModeTo = (targetMode: ListSortMode) => {
Expand All @@ -19,11 +18,18 @@ export const useSettingStore = defineStore('setting', () => {
// umami?.track(`switch list mode to ${targetMode}`).catch(() => {});
};

// 切换利润计算规则
const switchProfitComputeRuleTo = (targetRule: ProfitComputeRule) => {
profitComputeRule.value = targetRule;

// umami?.track(`switch profit compute rule to ${targetRule}`).catch(() => {});
/**
* 根据设置中的配置计算利润
* @param sourceCityPrice 买入价格
* @param targetCityPrice 卖出价格
* @returns
*/
const getProfitWithRule = (sourceCityPrice: number | undefined, targetCityPrice: number) => {
// 买入价格不存在时,返回-9999
if (!sourceCityPrice) return -9999;
const finalSourceCityPrice = sourceCityPrice * (1 - priceChangeRate.value) * (1 + taxRate.value);
const finalTargetCityPrice = targetCityPrice * (1 + priceChangeRate.value) * (1 - taxRate.value);
return Math.round(finalTargetCityPrice - finalSourceCityPrice);
};

// 切换数据显示项
Expand All @@ -37,13 +43,11 @@ export const useSettingStore = defineStore('setting', () => {

return {
listSortMode: listSortMode,
// listSortMode: skipHydrate(listSortMode),
switchListSortModeTo,
profitComputeRule: profitComputeRule,
// profitComputeRule: skipHydrate(profitComputeRule),
switchProfitComputeRuleTo,
dataDisplayItems: dataDisplayItems,
// dataDisplayItems: skipHydrate(dataDisplayItems),
taxRate,
priceChangeRate,
getProfitWithRule,
dataDisplayItems,
switchDataDisplayItems
};
});
2 changes: 1 addition & 1 deletion utils/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export function isLogValid(log: Log | undefined | null) {
if (!log) return false;
const uploadedAt = log.uploadedAt.getTime();
// 60 分钟
return new Date().getTime() - uploadedAt > 3600 * 1000;
return new Date().getTime() - uploadedAt < 3600 * 1000;
}

0 comments on commit 5d09aa2

Please sign in to comment.