mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Update
This commit is contained in:
parent
5ac0f7ad64
commit
9f296e337b
70
src/components/ControlStrip.vue
Normal file
70
src/components/ControlStrip.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { useFileDialog } from '@vueuse/core';
|
||||
|
||||
withDefaults(defineProps<{ size?: 'small' | 'medium' | 'large'; round?: boolean }>(), {
|
||||
size: 'medium',
|
||||
round: false,
|
||||
});
|
||||
|
||||
const fileDialog = useFileDialog({
|
||||
accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
multiple: false,
|
||||
});
|
||||
fileDialog.onChange((files) => {
|
||||
const file = files?.item(0);
|
||||
file && emit('import', file);
|
||||
fileDialog.reset();
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
clear: [];
|
||||
import: [file: File];
|
||||
export: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="control-strip">
|
||||
<n-button-group class="button-group">
|
||||
<n-popconfirm
|
||||
placement="bottom"
|
||||
@positive-click="emit('clear')"
|
||||
positive-text="确定"
|
||||
negative-text="取消"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button type="default" ghost :round="round" :size="size">
|
||||
<template #icon>
|
||||
<ion-trash-outline />
|
||||
</template>
|
||||
清空
|
||||
</n-button>
|
||||
</template>
|
||||
确认清空所有数据吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="default" ghost :round="round" @click="fileDialog.open()" :size="size">
|
||||
<template #icon>
|
||||
<gg-import />
|
||||
</template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button type="default" ghost :round="round" :size="size" @click="emit('export')">
|
||||
<template #icon>
|
||||
<ion-arrow-up-right-box-outline />
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-popover v-if="$slots.filter" trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-button type="default" ghost :round="round" :size="size">
|
||||
<template #icon>
|
||||
<ant-design-filter-outlined />
|
||||
</template>
|
||||
过滤
|
||||
</n-button>
|
||||
</template>
|
||||
<slot name="filter" />
|
||||
</n-popover>
|
||||
</n-button-group>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,21 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||
import { reviewItems } from '~/logic/storage';
|
||||
import ReviewList from './ReviewList.vue';
|
||||
import ReviewPreview from './ReviewPreview.vue';
|
||||
|
||||
const props = defineProps<{ model: AmazonDetailItem }>();
|
||||
|
||||
const modal = useModal();
|
||||
const handleLoadMore = () => {
|
||||
modal.create({
|
||||
title: '评论',
|
||||
title: `${props.model.asin}全部评论`,
|
||||
preset: 'card',
|
||||
style: {
|
||||
width: '85vw',
|
||||
width: '80vw',
|
||||
height: '85vh',
|
||||
},
|
||||
content: () =>
|
||||
h(ReviewList, {
|
||||
h(ReviewPreview, {
|
||||
asin: props.model.asin,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -5,20 +5,9 @@ import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
||||
import type { AmazonDetailItem, AmazonItem } from '~/logic/page-worker/types';
|
||||
import { allItems } from '~/logic/storage';
|
||||
import DetailDescription from './DetailDescription.vue';
|
||||
import { useFileDialog } from '@vueuse/core';
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const fileDialog = useFileDialog({
|
||||
accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
multiple: false,
|
||||
});
|
||||
fileDialog.onChange((files) => {
|
||||
const file = files?.item(0);
|
||||
file && handleImport(file);
|
||||
fileDialog.reset();
|
||||
});
|
||||
|
||||
const page = reactive({ current: 1, size: 10 });
|
||||
|
||||
const filter = reactive({
|
||||
@ -162,12 +151,6 @@ const extraHeaders: Header[] = [
|
||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
||||
parseImportValue: (val?: string) => val?.split(';'),
|
||||
},
|
||||
{
|
||||
prop: 'topReviews',
|
||||
label: '精选评论',
|
||||
formatOutputValue: (val?: Record<string, any>[]) => JSON.stringify(val),
|
||||
parseImportValue: (val?: string) => val && JSON.parse(val),
|
||||
},
|
||||
];
|
||||
|
||||
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
||||
@ -248,44 +231,14 @@ const handleClearData = async () => {
|
||||
round
|
||||
style="min-width: 230px"
|
||||
/>
|
||||
<n-button-group class="button-group">
|
||||
<n-popconfirm
|
||||
placement="bottom"
|
||||
@positive-click="handleClearData"
|
||||
positive-text="确定"
|
||||
negative-text="取消"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button type="default" ghost round size="small">
|
||||
<template #icon>
|
||||
<ion-trash-outline />
|
||||
</template>
|
||||
清空
|
||||
</n-button>
|
||||
</template>
|
||||
确认清空所有数据吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="default" ghost round @click="fileDialog.open()" size="small">
|
||||
<template #icon>
|
||||
<gg-import />
|
||||
</template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button type="default" ghost round size="small" @click="handleExport">
|
||||
<template #icon>
|
||||
<ion-arrow-up-right-box-outline />
|
||||
</template>
|
||||
导出
|
||||
</n-button>
|
||||
<n-popover trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-button type="default" ghost round size="small">
|
||||
<template #icon>
|
||||
<ant-design-filter-outlined />
|
||||
</template>
|
||||
过滤
|
||||
</n-button>
|
||||
</template>
|
||||
<control-strip
|
||||
round
|
||||
size="small"
|
||||
@clear="handleClearData"
|
||||
@export="handleExport"
|
||||
@import="handleImport"
|
||||
>
|
||||
<template #filter>
|
||||
<div class="filter-section">
|
||||
<div class="filter-title">筛选器</div>
|
||||
<n-form :model="filter" label-placement="left">
|
||||
@ -301,11 +254,11 @@ const handleClearData = async () => {
|
||||
</n-form>
|
||||
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</n-button-group>
|
||||
</template>
|
||||
</control-strip>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-empty v-if="allItems.length === 0" size="huge">
|
||||
<n-empty v-if="itemView.records.length === 0" size="huge">
|
||||
<template #icon>
|
||||
<n-icon size="60">
|
||||
<solar-cat-linear />
|
||||
@ -356,7 +309,7 @@ const handleClearData = async () => {
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
|
||||
.filter-title {
|
||||
font-size: 18px;
|
||||
|
||||
@ -9,7 +9,7 @@ defineProps<{
|
||||
<template>
|
||||
<div class="review-card">
|
||||
<h3 style="margin: 0">{{ model.username }}: {{ model.title }}</h3>
|
||||
<n-rate readonly size="small" :default-value="Number(model.rating.split(' ')[0])" />
|
||||
<n-rate readonly size="small" :value="Number(model.rating.split('.')[0])" />
|
||||
<div v-for="paragraph in model.content.split('\n')">
|
||||
{{ paragraph }}
|
||||
</div>
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
||||
import { reviewItems } from '~/logic/storage';
|
||||
|
||||
const props = defineProps<{ asin: string }>();
|
||||
|
||||
const containerRef = useTemplateRef('review-list');
|
||||
const { height } = useElementSize(containerRef);
|
||||
|
||||
const allReviews = reviewItems.value.get(props.asin) || [];
|
||||
|
||||
const filter = reactive({
|
||||
keywords: '',
|
||||
});
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const view = computed(() => {
|
||||
const offset = (page.current - 1) * page.pageSize;
|
||||
const filteredData = filterData(allReviews);
|
||||
const data = filteredData.slice(offset, offset + page.pageSize);
|
||||
const pageCount = ~~(filteredData.length / page.pageSize);
|
||||
return { data, pageCount, total: filteredData.length };
|
||||
});
|
||||
|
||||
const filterData = (data: AmazonReview[]) => {
|
||||
let filteredData = data;
|
||||
if (filter.keywords) {
|
||||
filteredData = data.filter((item) => {
|
||||
const keywords = filter.keywords.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(keywords) ||
|
||||
item.content.toLowerCase().includes(keywords) ||
|
||||
item.username.toLowerCase().includes(keywords)
|
||||
);
|
||||
});
|
||||
}
|
||||
return filteredData;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="review-list" ref="review-list">
|
||||
<div class="header">
|
||||
<div class="header-section">
|
||||
<n-space justify="start">
|
||||
<n-input
|
||||
v-model:value="filter.keywords"
|
||||
style="width: 500px"
|
||||
round
|
||||
placeholder="输入关键词筛选评论"
|
||||
/>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="header-section"></div>
|
||||
</div>
|
||||
<n-scrollbar :style="{ maxHeight: `${height * 0.85}px`, minHeight: `${height * 0.85}px` }">
|
||||
<div class="review-list-container" :style="{ minHeight: `${height * 0.8}px` }">
|
||||
<template v-for="review in view.data">
|
||||
<review-card :model="review" />
|
||||
<div style="height: 7px" />
|
||||
</template>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<div>
|
||||
<n-space align="center">
|
||||
<n-pagination
|
||||
v-model:page="page.current"
|
||||
v-model:page-size="page.pageSize"
|
||||
:page-count="view.pageCount"
|
||||
/>
|
||||
<n-text>{{ view.total }}条评论</n-text>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.review-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: flex-start;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.review-list-container {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
205
src/components/ReviewPreview.vue
Normal file
205
src/components/ReviewPreview.vue
Normal file
@ -0,0 +1,205 @@
|
||||
<script lang="ts" setup>
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
||||
import { reviewItems } from '~/logic/storage';
|
||||
|
||||
const props = defineProps<{ asin: string }>();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const containerRef = useTemplateRef('review-list');
|
||||
const { height } = useElementSize(containerRef);
|
||||
|
||||
const allReviews = reviewItems.value.get(props.asin) || [];
|
||||
|
||||
const filter = reactive({
|
||||
keywords: '',
|
||||
rating: null as string | null,
|
||||
});
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const view = computed(() => {
|
||||
const filteredData = filterData(allReviews);
|
||||
const offset = (page.current - 1) * page.pageSize;
|
||||
if (offset >= filteredData.length && page.current > 1) {
|
||||
page.current = 1;
|
||||
}
|
||||
const data = filteredData.slice(offset, offset + page.pageSize);
|
||||
const pageCount = ~~(filteredData.length / page.pageSize);
|
||||
return { data, pageCount, total: filteredData.length };
|
||||
});
|
||||
|
||||
const filterData = (data: AmazonReview[]) => {
|
||||
let filteredData = data;
|
||||
if (filter.keywords) {
|
||||
filteredData = data.filter((item) => {
|
||||
const keywords = filter.keywords.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(keywords) ||
|
||||
item.content.toLowerCase().includes(keywords) ||
|
||||
item.username.toLowerCase().includes(keywords)
|
||||
);
|
||||
});
|
||||
}
|
||||
if (filter.rating) {
|
||||
filteredData = filteredData.filter((item) => item.rating === filter.rating);
|
||||
}
|
||||
return filteredData;
|
||||
};
|
||||
|
||||
const handleClearData = () => {
|
||||
reviewItems.value.delete(props.asin);
|
||||
allReviews.splice(0, allReviews.length);
|
||||
page.current = 1;
|
||||
};
|
||||
|
||||
const headers: Header[] = [
|
||||
{ prop: 'username', label: '用户名' },
|
||||
{ prop: 'title', label: '标题' },
|
||||
{ prop: 'rating', label: '评分' },
|
||||
{ prop: 'content', label: '内容' },
|
||||
{ prop: 'dateInfo', label: '日期' },
|
||||
{
|
||||
prop: 'imageSrc',
|
||||
label: '图片链接',
|
||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
||||
parseImportValue: (val?: string) => val?.split(';'),
|
||||
},
|
||||
];
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
const importedData = await importFromXLSX<AmazonReview>(file, { headers });
|
||||
if (importedData.length === 0) {
|
||||
return;
|
||||
}
|
||||
const existingIds = new Set(allReviews.map((review) => review.id));
|
||||
const newReviews = importedData.filter((review) => !existingIds.has(review.id));
|
||||
if (newReviews.length > 0) {
|
||||
allReviews.push(...newReviews);
|
||||
allReviews.sort((a, b) => dayjs(b.dateInfo).valueOf() - dayjs(a.dateInfo).valueOf());
|
||||
reviewItems.value.delete(props.asin); // Clear existing data for this ASIN
|
||||
reviewItems.value.set(props.asin, allReviews);
|
||||
page.current = 1; // Reset to first page after import
|
||||
message.info(`成功导入 ${file.name} 文件 ${newReviews.length} 条新评论`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
exportToXLSX(allReviews, { headers });
|
||||
message.info('导出完成');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="review-list" ref="review-list">
|
||||
<div class="header">
|
||||
<div class="header-section">
|
||||
<n-space justify="start">
|
||||
<n-input
|
||||
v-model:value="filter.keywords"
|
||||
style="width: 500px"
|
||||
placeholder="输入关键词筛选评论"
|
||||
/>
|
||||
</n-space>
|
||||
</div>
|
||||
<div class="header-section">
|
||||
<n-space justify="end">
|
||||
<n-select
|
||||
v-model:value="filter.rating"
|
||||
style="width: 200px"
|
||||
round
|
||||
placeholder="选择评分"
|
||||
clearable
|
||||
:options="
|
||||
Array.from({ length: 5 }).map((_, i) => ({
|
||||
label: `${i + 1} 星 ${'★'.repeat(i + 1)}`,
|
||||
value: `${i + 1}.0 out of 5 stars`,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
<control-strip @import="handleImport" @export="handleExport" @clear="handleClearData" />
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
<n-scrollbar :style="{ maxHeight: `${height * 0.85}px`, minHeight: `${height * 0.85}px` }">
|
||||
<div class="review-list-container" :style="{ minHeight: `${height * 0.8}px` }">
|
||||
<template v-if="view.data.length > 0" v-for="review in view.data">
|
||||
<review-card :model="review" />
|
||||
<div style="height: 7px" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="empty-container">
|
||||
<n-icon size="60">
|
||||
<solar-cat-linear />
|
||||
</n-icon>
|
||||
<h3>还没有数据哦</h3>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<div>
|
||||
<n-space align="center">
|
||||
<n-pagination
|
||||
v-model:page="page.current"
|
||||
v-model:page-size="page.pageSize"
|
||||
:page-count="view.pageCount"
|
||||
/>
|
||||
<n-text>共{{ view.total }}条评论</n-text>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.review-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: flex-start;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.review-list-container {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888888ad;
|
||||
}
|
||||
</style>
|
||||
@ -156,20 +156,20 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
let rawRankingText: string | null = await injector.getRankText();
|
||||
if (rawRankingText) {
|
||||
const info: Pick<AmazonDetailItem, 'category1' | 'category2'> = {};
|
||||
let statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
let statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)\-]+/.exec(rawRankingText)?.[0];
|
||||
if (statement) {
|
||||
const name = /(?<=in\s).+(?=\s\(See)/.exec(statement)?.[0] || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', '')) || null;
|
||||
if (name && rank) {
|
||||
const name = /(?<=in\s).+/.exec(statement)?.[0].replace(/\s\(See\sTop.+\)/, '');
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', ''));
|
||||
if (name && !Number.isNaN(rank)) {
|
||||
info['category1'] = { name, rank };
|
||||
}
|
||||
rawRankingText = rawRankingText.replace(statement, '');
|
||||
}
|
||||
statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)]+/.exec(rawRankingText)?.[0];
|
||||
statement = /#[0-9,]+\sin\s\S[\s\w',\.&\(\)\-]+/.exec(rawRankingText)?.[0];
|
||||
if (statement) {
|
||||
const name = /(?<=in\s).+/.exec(statement)?.[0].replace(/[\s]+$/, '') || null;
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', '')) || null;
|
||||
if (name && rank) {
|
||||
const name = /(?<=in\s).+/.exec(statement)?.[0].replace(/[\s]+$/, '');
|
||||
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replaceAll(',', ''));
|
||||
if (name && !Number.isNaN(rank)) {
|
||||
info['category2'] = { name, rank };
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,12 +15,12 @@ const theme: GlobalThemeOverrides = {
|
||||
<template>
|
||||
<!-- Naive UI Wrapper-->
|
||||
<n-config-provider :theme-overrides="theme">
|
||||
<n-modal-provider>
|
||||
<n-message-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<n-modal-provider>
|
||||
<options />
|
||||
</n-message-provider>
|
||||
</n-modal-provider>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
@ -15,12 +15,12 @@ const theme: GlobalThemeOverrides = {
|
||||
<template>
|
||||
<!-- Naive UI Wrapper-->
|
||||
<n-config-provider :theme-overrides="theme">
|
||||
<n-modal-provider>
|
||||
<n-message-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<n-modal-provider>
|
||||
<side-panel />
|
||||
</n-message-provider>
|
||||
</n-modal-provider>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
@ -100,7 +100,17 @@ export default defineConfig(({ command }) => ({
|
||||
sidepanel: r('src/sidepanel/index.html'),
|
||||
options: r('src/options/index.html'),
|
||||
},
|
||||
output: {},
|
||||
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('naive-ui')) return 'vendor-naive-ui';
|
||||
else if (id.includes('vue')) return 'vendor-vue';
|
||||
return 'vendor';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user