mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-06 15:27:02 +08:00
Update UI & Worker
This commit is contained in:
parent
a84f148743
commit
9aeb12c7bf
@ -20,4 +20,16 @@ if (USE_SIDE_PANEL) {
|
|||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('Azon Seeker installed');
|
console.log('Azon Seeker installed');
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: 'show-result',
|
||||||
|
title: '结果页',
|
||||||
|
contexts: ['action'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.onClicked.addListener((info) => {
|
||||||
|
if (info.menuItemId === 'show-result') {
|
||||||
|
browser.runtime.openOptionsPage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
112
src/components/AsinsInput.vue
Normal file
112
src/components/AsinsInput.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFileDialog } from '@vueuse/core';
|
||||||
|
import type { FormItemRule } from 'naive-ui';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelValue = defineModel<string>({ required: true });
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const formItemRef = useTemplateRef('detail-form-item');
|
||||||
|
const formItemRule: FormItemRule = {
|
||||||
|
required: true,
|
||||||
|
trigger: ['submit', 'blur'],
|
||||||
|
message: '请输入格式正确的ASIN',
|
||||||
|
validator: () => {
|
||||||
|
return modelValue.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileDialog = useFileDialog({ accept: '.txt', multiple: false });
|
||||||
|
fileDialog.onChange((fileList) => {
|
||||||
|
const file = fileList?.item(0);
|
||||||
|
file && handleImportAsin(file);
|
||||||
|
fileDialog.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleImportAsin = (file: File) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target?.result;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
modelValue.value = content;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'utf-8');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportAsin = () => {
|
||||||
|
const blob = new Blob([modelValue.value], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const filename = `asin-${new Date().toISOString()}.txt`;
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
message.info('导出完成');
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
validate: async () =>
|
||||||
|
new Promise<boolean>((resolve) => {
|
||||||
|
formItemRef.value?.validate({
|
||||||
|
trigger: 'submit',
|
||||||
|
callback: (errors) => {
|
||||||
|
if (errors) {
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="asin-input">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
|
||||||
|
<template #icon>
|
||||||
|
<gg-import />
|
||||||
|
</template>
|
||||||
|
导入
|
||||||
|
</n-button>
|
||||||
|
<n-button :disabled="disabled" @click="handleExportAsin" round size="small">
|
||||||
|
<template #icon>
|
||||||
|
<ion-arrow-up-right-box-outline />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<div style="height: 7px" />
|
||||||
|
<n-form-item ref="detail-form-item" label-placement="left" :rule="formItemRule">
|
||||||
|
<n-input
|
||||||
|
:disabled="disabled"
|
||||||
|
v-model:value="modelValue"
|
||||||
|
placeholder="输入ASINs"
|
||||||
|
type="textarea"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.asin-input {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -33,18 +33,34 @@ const props = defineProps<{ model: AmazonDetailItem }>();
|
|||||||
{{ link }}
|
{{ link }}
|
||||||
</div>
|
</div>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item v-if="props.model.topReviews" label="精选评论" :span="4">
|
<n-descriptions-item
|
||||||
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
|
v-if="props.model.topReviews && props.model.topReviews.length > 0"
|
||||||
<h3 style="margin: 0">{{ review.username }}: {{ review.title }}</h3>
|
label="精选评论"
|
||||||
<div style="color: gray; font-size: smaller">{{ review.rating }}</div>
|
:span="4"
|
||||||
<div v-for="paragraph in review.content.split('\n')">
|
>
|
||||||
{{ paragraph }}
|
<n-scrollbar style="max-height: 500px">
|
||||||
|
<div class="review-item-cotent">
|
||||||
|
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
|
||||||
|
<h3 style="margin: 0">{{ review.username }}: {{ review.title }}</h3>
|
||||||
|
<div style="color: gray; font-size: smaller">{{ review.rating }}</div>
|
||||||
|
<div v-for="paragraph in review.content.split('\n')">
|
||||||
|
{{ paragraph }}
|
||||||
|
</div>
|
||||||
|
<div style="color: gray; font-size: smaller">{{ review.dateInfo }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="color: gray; font-size: smaller">{{ review.dateInfo }}</div>
|
</n-scrollbar>
|
||||||
|
<div class="review-item-footer">
|
||||||
|
<n-button size="small">Load More</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
.review-item-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,14 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NButton, UploadOnChange } from 'naive-ui';
|
import { NButton } from 'naive-ui';
|
||||||
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
|
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
|
||||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
||||||
import type { AmazonDetailItem, AmazonItem } from '~/logic/page-worker/types';
|
import type { AmazonDetailItem, AmazonItem } from '~/logic/page-worker/types';
|
||||||
import { allItems } from '~/logic/storage';
|
import { allItems } from '~/logic/storage';
|
||||||
import DetailDescription from './DetailDescription.vue';
|
import DetailDescription from './DetailDescription.vue';
|
||||||
|
import { useFileDialog } from '@vueuse/core';
|
||||||
|
|
||||||
const message = useMessage();
|
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 page = reactive({ current: 1, size: 10 });
|
||||||
|
|
||||||
const filter = reactive({
|
const filter = reactive({
|
||||||
keywords: null as string | null,
|
keywords: null as string | null,
|
||||||
search: '',
|
search: '',
|
||||||
@ -120,10 +132,14 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: AmazonItem[] }>(
|
const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: AmazonItem[] }>(
|
||||||
() => {
|
() => {
|
||||||
const { current, size } = page;
|
const { current, size } = page;
|
||||||
let data = filterItemData(allItems.value); // Filter Data
|
const data = filterItemData(allItems.value); // Filter Data
|
||||||
let pageCount = ~~(data.length / size);
|
const pageCount = ~~(data.length / size) + (data.length % size > 0 ? 1 : 0);
|
||||||
pageCount += data.length % size > 0 ? 1 : 0;
|
const offset = (current - 1) * size;
|
||||||
return { records: data.slice((current - 1) * size, current * size), pageCount, origin: data };
|
if (data.length > 0 && offset >= data.length) {
|
||||||
|
page.current = 1;
|
||||||
|
}
|
||||||
|
const records = data.slice(offset, offset + size);
|
||||||
|
return { records, pageCount, origin: data };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -189,11 +205,7 @@ const handleExport = async () => {
|
|||||||
message.info('导出完成');
|
message.info('导出完成');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport: UploadOnChange = async ({ fileList }) => {
|
const handleImport = async (file: File) => {
|
||||||
if (fileList.length !== 1 || fileList[0].file === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file: File = fileList.pop()!.file!;
|
|
||||||
const headers: Header[] = columns
|
const headers: Header[] = columns
|
||||||
.reduce(
|
.reduce(
|
||||||
(p, v: Record<string, any>) => {
|
(p, v: Record<string, any>) => {
|
||||||
@ -234,65 +246,66 @@ const handleClearData = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
placeholder="输入文本过滤结果"
|
placeholder="输入文本过滤结果"
|
||||||
round
|
round
|
||||||
|
style="min-width: 230px"
|
||||||
/>
|
/>
|
||||||
<n-popconfirm
|
<n-button-group class="button-group">
|
||||||
placement="bottom"
|
<n-popconfirm
|
||||||
@positive-click="handleClearData"
|
placement="bottom"
|
||||||
positive-text="确定"
|
@positive-click="handleClearData"
|
||||||
negative-text="取消"
|
positive-text="确定"
|
||||||
>
|
negative-text="取消"
|
||||||
<template #trigger>
|
>
|
||||||
<n-button type="error" tertiary circle size="small">
|
<template #trigger>
|
||||||
<template #icon>
|
<n-button type="default" ghost round size="small">
|
||||||
<ion-trash-outline />
|
<template #icon>
|
||||||
</template>
|
<ion-trash-outline />
|
||||||
</n-button>
|
</template>
|
||||||
</template>
|
清空
|
||||||
确认清空所有数据吗?
|
</n-button>
|
||||||
</n-popconfirm>
|
</template>
|
||||||
<n-upload
|
确认清空所有数据吗?
|
||||||
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
</n-popconfirm>
|
||||||
:max="1"
|
<n-button type="default" ghost round @click="fileDialog.open()" size="small">
|
||||||
@change="handleImport"
|
|
||||||
>
|
|
||||||
<n-button type="primary" tertiary circle size="small">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<gg-import />
|
<gg-import />
|
||||||
</template>
|
</template>
|
||||||
|
导入
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-upload>
|
<n-button type="default" ghost round size="small" @click="handleExport">
|
||||||
<n-button type="primary" tertiary circle size="small" @click="handleExport">
|
<template #icon>
|
||||||
<template #icon>
|
<ion-arrow-up-right-box-outline />
|
||||||
<ion-arrow-up-right-box-outline />
|
</template>
|
||||||
</template>
|
导出
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-popover trigger="hover" placement="bottom">
|
<n-popover trigger="hover" placement="bottom">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button type="primary" tertiary circle size="small">
|
<n-button type="default" ghost round size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-filter-outlined />
|
<ant-design-filter-outlined />
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
过滤
|
||||||
</template>
|
</n-button>
|
||||||
<div class="filter-section">
|
</template>
|
||||||
<div class="filter-title">筛选器</div>
|
<div class="filter-section">
|
||||||
<n-form :model="filter" label-placement="left">
|
<div class="filter-title">筛选器</div>
|
||||||
<n-form-item v-for="item in filterFormItems" :label="item.label">
|
<n-form :model="filter" label-placement="left">
|
||||||
<n-select
|
<n-form-item v-for="item in filterFormItems" :label="item.label">
|
||||||
v-if="item.type === 'select'"
|
<n-select
|
||||||
placeholder=""
|
v-if="item.type === 'select'"
|
||||||
v-model:value="filter.keywords"
|
placeholder=""
|
||||||
clearable
|
v-model:value="filter.keywords"
|
||||||
:options="item.params.options"
|
clearable
|
||||||
/>
|
:options="item.params.options"
|
||||||
</n-form-item>
|
/>
|
||||||
</n-form>
|
</n-form-item>
|
||||||
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
|
</n-form>
|
||||||
</div>
|
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
|
||||||
</n-popover>
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</n-button-group>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
<n-empty v-if="allItems.length === 0" size="huge" style="padding-top: 40px">
|
<n-empty v-if="allItems.length === 0" size="huge">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="60">
|
<n-icon size="60">
|
||||||
<solar-cat-linear />
|
<solar-cat-linear />
|
||||||
@ -324,7 +337,13 @@ const handleClearData = async () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.result-content-container {
|
.result-content-container {
|
||||||
width: 100%;
|
min-height: 100%;
|
||||||
|
:deep(.n-card__content:has(.n-empty)) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.filter-switch) {
|
:deep(.filter-switch) {
|
||||||
|
|||||||
21
src/composables/useLongTask.ts
Normal file
21
src/composables/useLongTask.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function useLongTask() {
|
||||||
|
const isRunning = ref(false);
|
||||||
|
|
||||||
|
const startTask = async (task: () => Promise<void>) => {
|
||||||
|
isRunning.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await task();
|
||||||
|
isRunning.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
isRunning.value = false;
|
||||||
|
console.error('Task failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRunning,
|
||||||
|
startTask,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -37,15 +37,16 @@ export async function exec<T, P extends Record<string, unknown>>(
|
|||||||
};
|
};
|
||||||
return new Promise<T>(async (resolve, reject) => {
|
return new Promise<T>(async (resolve, reject) => {
|
||||||
setTimeout(() => reject('脚本运行超时'), timeout);
|
setTimeout(() => reject('脚本运行超时'), timeout);
|
||||||
const injectResults = await browser.scripting.executeScript({
|
try {
|
||||||
target: { tabId },
|
const injectResults = await browser.scripting.executeScript({
|
||||||
func,
|
target: { tabId },
|
||||||
args: payload ? [payload] : undefined,
|
func,
|
||||||
});
|
args: payload ? [payload] : undefined,
|
||||||
const ret = injectResults.pop();
|
});
|
||||||
if (ret?.error) {
|
const ret = injectResults.pop();
|
||||||
reject(`注入脚本时发生错误: ${ret.error}`); // 似乎无法走到这一步
|
resolve(ret!.result as T);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`入脚本运行失败: ${e}`);
|
||||||
}
|
}
|
||||||
resolve(ret!.result as T);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/logic/page-worker/types.d.ts
vendored
1
src/logic/page-worker/types.d.ts
vendored
@ -32,6 +32,7 @@ type AmazonReview = {
|
|||||||
rating: string;
|
rating: string;
|
||||||
dateInfo: string;
|
dateInfo: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
imageSrc: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
|
type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import type { AmazonDetailItem, AmazonItem, AmazonSearchItem } from './page-work
|
|||||||
|
|
||||||
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
||||||
|
|
||||||
export const asinInputText = useWebExtensionStorage<string>('asinInputText', '');
|
export const detailAsinInput = useWebExtensionStorage<string>('detailAsinInputText', '');
|
||||||
|
|
||||||
|
export const reviewAsinInput = useWebExtensionStorage<string>('reviewAsinInputText', '');
|
||||||
|
|
||||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||||
|
|
||||||
@ -17,8 +19,8 @@ export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>
|
|||||||
|
|
||||||
export const allItems = computed({
|
export const allItems = computed({
|
||||||
get() {
|
get() {
|
||||||
const sItems = searchItems.value;
|
const sItems = unref(searchItems);
|
||||||
const dItems = detailItems.value;
|
const dItems = unref(detailItems);
|
||||||
return sItems.map<AmazonItem>((si) => {
|
return sItems.map<AmazonItem>((si) => {
|
||||||
const asin = si.asin;
|
const asin = si.asin;
|
||||||
const dItem = dItems.get(asin);
|
const dItem = dItems.get(asin);
|
||||||
@ -45,6 +47,8 @@ export const allItems = computed({
|
|||||||
});
|
});
|
||||||
const detailItemsProps: (keyof AmazonDetailItem)[] = [
|
const detailItemsProps: (keyof AmazonDetailItem)[] = [
|
||||||
'asin',
|
'asin',
|
||||||
|
'title',
|
||||||
|
'price',
|
||||||
'category1',
|
'category1',
|
||||||
'category2',
|
'category2',
|
||||||
'imageUrls',
|
'imageUrls',
|
||||||
|
|||||||
@ -306,7 +306,12 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
const content = commentNode.querySelector<HTMLDivElement>(
|
const content = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-body"]',
|
'[data-hook="review-body"]',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
items.push({ id, username, title, rating, dateInfo, content });
|
const imageSrc = Array.from(
|
||||||
|
commentNode.querySelectorAll<HTMLImageElement>(
|
||||||
|
'.review-image-tile-section img[src] img[src]',
|
||||||
|
),
|
||||||
|
).map((e) => e.getAttribute('src')!);
|
||||||
|
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
@ -377,7 +382,10 @@ export class AmazonReviewPageInjector extends BaseInjector {
|
|||||||
const content = commentNode.querySelector<HTMLDivElement>(
|
const content = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-body"]',
|
'[data-hook="review-body"]',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
items.push({ id, username, title, rating, dateInfo, content });
|
const imageSrc = Array.from(
|
||||||
|
commentNode.querySelectorAll<HTMLImageElement>('.review-image-tile-section img[src]'),
|
||||||
|
).map((e) => e.getAttribute('src')!);
|
||||||
|
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { Manifest } from 'webextension-polyfill';
|
import { type Manifest } from 'webextension-polyfill';
|
||||||
import type PkgType from '../package.json';
|
import type PkgType from '../package.json';
|
||||||
import { isDev, isFirefox, port, r } from '../scripts/utils';
|
import { isDev, isFirefox, port, r } from '../scripts/utils';
|
||||||
|
|
||||||
@ -33,7 +33,15 @@ export async function getManifest() {
|
|||||||
48: './assets/icon-512.png',
|
48: './assets/icon-512.png',
|
||||||
128: './assets/icon-512.png',
|
128: './assets/icon-512.png',
|
||||||
},
|
},
|
||||||
permissions: ['tabs', 'storage', 'activeTab', 'sidePanel', 'scripting', 'unlimitedStorage'],
|
permissions: [
|
||||||
|
'tabs',
|
||||||
|
'storage',
|
||||||
|
'activeTab',
|
||||||
|
'sidePanel',
|
||||||
|
'scripting',
|
||||||
|
'unlimitedStorage',
|
||||||
|
'contextMenus',
|
||||||
|
],
|
||||||
host_permissions: ['*://*/*'],
|
host_permissions: ['*://*/*'],
|
||||||
content_scripts: [
|
content_scripts: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,7 +14,8 @@ main {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 95%;
|
height: 90vh;
|
||||||
|
width: 95vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import SidePanel from './SidePanel.vue';
|
import SidePanel from './Sidepanel.vue';
|
||||||
|
|
||||||
const theme: GlobalThemeOverrides = {
|
const theme: GlobalThemeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
|
|||||||
@ -1,21 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormItemRule, UploadOnChange } from 'naive-ui';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import pageWorker from '~/logic/page-worker';
|
import pageWorker from '~/logic/page-worker';
|
||||||
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||||
import { asinInputText, detailItems } from '~/logic/storage';
|
import { detailAsinInput, detailItems } from '~/logic/storage';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const formItemRef = useTemplateRef('detail-form-item');
|
|
||||||
const formItemRule: FormItemRule = {
|
|
||||||
required: true,
|
|
||||||
trigger: ['submit', 'blur'],
|
|
||||||
message: '请输入格式正确的ASIN',
|
|
||||||
validator: () => {
|
|
||||||
return asinInputText.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<
|
||||||
{
|
{
|
||||||
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
||||||
@ -25,7 +15,9 @@ const timelines = ref<
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const running = ref(false);
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
|
const asinInputRef = useTemplateRef('asin-input');
|
||||||
|
|
||||||
//#region Page Worker 初始化Code
|
//#region Page Worker 初始化Code
|
||||||
const worker = pageWorker.useAmazonPageWorker(); // 获取Page Worker单例
|
const worker = pageWorker.useAmazonPageWorker(); // 获取Page Worker单例
|
||||||
@ -37,7 +29,7 @@ worker.channel.on('error', ({ message: msg }) => {
|
|||||||
content: msg,
|
content: msg,
|
||||||
});
|
});
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
running.value = false;
|
worker.stop();
|
||||||
});
|
});
|
||||||
worker.channel.on('item-base-info-collected', (ev) => {
|
worker.channel.on('item-base-info-collected', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
@ -98,72 +90,33 @@ const updateDetailItems = (row: { asin: string } & Partial<AmazonDetailItem>) =>
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
const task = async () => {
|
||||||
if (fileList.length > 0) {
|
const asinList = detailAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
||||||
const file = fileList.pop();
|
timelines.value = [
|
||||||
if (file && file.file) {
|
{
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const content = e.target?.result;
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
asinInputText.value = content;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file.file, 'utf-8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportAsin = () => {
|
|
||||||
const blob = new Blob([asinInputText.value], { type: 'text/plain' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const filename = `asin-${new Date().toISOString()}.txt`;
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
message.info('导出完成');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFetchInfoFromPage = () => {
|
|
||||||
const runTask = async () => {
|
|
||||||
const asinList = asinInputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
|
||||||
running.value = true;
|
|
||||||
timelines.value = [
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
title: '开始',
|
|
||||||
time: new Date().toLocaleString(),
|
|
||||||
content: '开始数据采集',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await worker.runDetaiPageTask(asinList, async (remains) => {
|
|
||||||
asinInputText.value = remains.join('\n');
|
|
||||||
});
|
|
||||||
timelines.value.push({
|
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '结束',
|
title: '开始',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: '数据采集完成',
|
content: '开始数据采集',
|
||||||
});
|
|
||||||
running.value = false;
|
|
||||||
};
|
|
||||||
formItemRef.value?.validate({
|
|
||||||
callback: (errors) => {
|
|
||||||
if (errors) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
runTask();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
await worker.runDetaiPageTask(asinList, async (remains) => {
|
||||||
|
detailAsinInput.value = remains.join('\n');
|
||||||
|
});
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'info',
|
||||||
|
title: '结束',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: '数据采集完成',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
asinInputRef.value?.validate().then(async (success) => success && startTask(task));
|
||||||
|
};
|
||||||
|
|
||||||
const handleInterrupt = () => {
|
const handleInterrupt = () => {
|
||||||
if (!running.value) return;
|
if (!isRunning.value) return;
|
||||||
worker.stop();
|
worker.stop();
|
||||||
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
};
|
};
|
||||||
@ -173,37 +126,8 @@ const handleInterrupt = () => {
|
|||||||
<div class="detail-page-entry">
|
<div class="detail-page-entry">
|
||||||
<header-title>Detail Page</header-title>
|
<header-title>Detail Page</header-title>
|
||||||
<div class="interative-section">
|
<div class="interative-section">
|
||||||
<n-space>
|
<asins-input v-model="detailAsinInput" :disabled="isRunning" ref="asin-input" />
|
||||||
<n-upload @change="handleImportAsin" accept=".txt" :max="1">
|
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
||||||
<n-button :disabled="running" round size="small">
|
|
||||||
<template #icon>
|
|
||||||
<gg-import />
|
|
||||||
</template>
|
|
||||||
导入
|
|
||||||
</n-button>
|
|
||||||
</n-upload>
|
|
||||||
<n-button :disabled="running" @click="handleExportAsin" round size="small">
|
|
||||||
<template #icon>
|
|
||||||
<ion-arrow-up-right-box-outline />
|
|
||||||
</template>
|
|
||||||
导出
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<n-form-item
|
|
||||||
ref="detail-form-item"
|
|
||||||
label-placement="left"
|
|
||||||
:rule="formItemRule"
|
|
||||||
style="padding-top: 0px"
|
|
||||||
>
|
|
||||||
<n-input
|
|
||||||
:disabled="running"
|
|
||||||
v-model:value="asinInputText"
|
|
||||||
placeholder="输入ASINs"
|
|
||||||
type="textarea"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-button v-if="!running" round size="large" type="primary" @click="handleFetchInfoFromPage">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
</template>
|
</template>
|
||||||
@ -216,7 +140,7 @@ const handleInterrupt = () => {
|
|||||||
停止
|
停止
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="running" class="running-tip-section">
|
<div v-if="isRunning" class="running-tip-section">
|
||||||
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
</div>
|
</div>
|
||||||
<progress-report class="progress-report" :timelines="timelines" />
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
@ -230,29 +154,29 @@ const handleInterrupt = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.interative-section {
|
.interative-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px #00000020 dashed;
|
border: 1px #00000020 dashed;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.running-tip-section {
|
.running-tip-section {
|
||||||
margin: 10px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-report {
|
.progress-report {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,35 +1,124 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import pageWorker from '~/logic/page-worker';
|
import pageWorker from '~/logic/page-worker';
|
||||||
|
import { reviewAsinInput } from '~/logic/storage';
|
||||||
|
|
||||||
const worker = pageWorker.useAmazonPageWorker();
|
const worker = pageWorker.useAmazonPageWorker();
|
||||||
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'error',
|
||||||
|
title: '错误发生',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: msg,
|
||||||
|
});
|
||||||
|
});
|
||||||
worker.channel.on('item-review-collected', (ev) => {
|
worker.channel.on('item-review-collected', (ev) => {
|
||||||
output.value = ev;
|
timelines.value.push({
|
||||||
|
type: 'success',
|
||||||
|
title: `商品${ev.asin}评价`,
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `获取到 ${ev.reviews.length} 条评价`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputText = ref('');
|
const { isRunning, startTask } = useLongTask();
|
||||||
const output = ref<any>(null);
|
|
||||||
|
const asinInputRef = useTemplateRef('asin-input');
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const timelines = ref<
|
||||||
|
{
|
||||||
|
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
||||||
|
title: string;
|
||||||
|
time: string;
|
||||||
|
content: string;
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const task = async () => {
|
||||||
|
const asinList = reviewAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
||||||
|
timelines.value = [
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
title: '开始',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: '开始数据采集',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await worker.runReviewPageTask(asinList, async (remains) => {
|
||||||
|
reviewAsinInput.value = remains.join('\n');
|
||||||
|
});
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'info',
|
||||||
|
title: '结束',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: '数据采集完成',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
worker.runReviewPageTask(inputText.value.split('\n').filter((t) => /^[A-Z0-9]{10}/.exec(t)));
|
asinInputRef.value?.validate().then(async (success) => success && startTask(task));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInterrupt = () => {
|
||||||
|
worker.stop();
|
||||||
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="review-page-entry">
|
<div class="review-page-entry">
|
||||||
<header-title>Review Page</header-title>
|
<header-title>Review Page</header-title>
|
||||||
<n-input type="textarea" v-model:value="inputText" />
|
<div class="interative-section">
|
||||||
<n-button @click="handleStart">测试</n-button>
|
<asins-input v-model="reviewAsinInput" :disabled="isRunning" ref="asin-input" />
|
||||||
<div>{{ output }}</div>
|
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
||||||
|
<template #icon>
|
||||||
|
<ant-design-thunderbolt-outlined />
|
||||||
|
</template>
|
||||||
|
开始
|
||||||
|
</n-button>
|
||||||
|
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||||
|
<template #icon>
|
||||||
|
<ant-design-thunderbolt-outlined />
|
||||||
|
</template>
|
||||||
|
停止
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isRunning" class="running-tip-section">
|
||||||
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
|
</div>
|
||||||
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.review-page-entry {
|
.review-page-entry {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
}
|
||||||
|
|
||||||
|
.interative-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 15px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
width: 85%;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px #00000020 dashed;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.running-tip-section {
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-report {
|
||||||
|
width: 90%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,8 +3,11 @@ import { keywordsList } from '~/logic/storage';
|
|||||||
import pageWorker from '~/logic/page-worker';
|
import pageWorker from '~/logic/page-worker';
|
||||||
import { NButton } from 'naive-ui';
|
import { NButton } from 'naive-ui';
|
||||||
import { searchItems } from '~/logic/storage';
|
import { searchItems } from '~/logic/storage';
|
||||||
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
//#region Initial Page Worker
|
//#region Initial Page Worker
|
||||||
const worker = pageWorker.useAmazonPageWorker();
|
const worker = pageWorker.useAmazonPageWorker();
|
||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
@ -16,7 +19,6 @@ worker.channel.on('error', ({ message: msg }) => {
|
|||||||
});
|
});
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
worker.stop();
|
worker.stop();
|
||||||
running.value = false;
|
|
||||||
});
|
});
|
||||||
worker.channel.on('item-links-collected', ({ objs }) => {
|
worker.channel.on('item-links-collected', ({ objs }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
@ -28,7 +30,6 @@ worker.channel.on('item-links-collected', ({ objs }) => {
|
|||||||
searchItems.value = searchItems.value.concat(objs); // Add records
|
searchItems.value = searchItems.value.concat(objs); // Add records
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
const running = ref(false);
|
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<
|
||||||
{
|
{
|
||||||
@ -39,12 +40,8 @@ const timelines = ref<
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const handleFetchInfoFromPage = async () => {
|
const task = async () => {
|
||||||
if (keywordsList.value.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const kws = unref(keywordsList);
|
const kws = unref(keywordsList);
|
||||||
running.value = true;
|
|
||||||
timelines.value = [
|
timelines.value = [
|
||||||
{
|
{
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -71,11 +68,17 @@ const handleFetchInfoFromPage = async () => {
|
|||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `搜索任务结束`,
|
content: `搜索任务结束`,
|
||||||
});
|
});
|
||||||
running.value = false;
|
};
|
||||||
|
|
||||||
|
const handleStart = async () => {
|
||||||
|
if (keywordsList.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startTask(task);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInterrupt = () => {
|
const handleInterrupt = () => {
|
||||||
if (!running.value) return;
|
if (!isRunning.value) return;
|
||||||
worker.stop();
|
worker.stop();
|
||||||
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
};
|
};
|
||||||
@ -86,7 +89,7 @@ const handleInterrupt = () => {
|
|||||||
<header-title>Search Page</header-title>
|
<header-title>Search Page</header-title>
|
||||||
<div class="interactive-section">
|
<div class="interactive-section">
|
||||||
<n-dynamic-input
|
<n-dynamic-input
|
||||||
:disabled="running"
|
:disabled="isRunning"
|
||||||
v-model:value="keywordsList"
|
v-model:value="keywordsList"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10"
|
:max="10"
|
||||||
@ -96,7 +99,7 @@ const handleInterrupt = () => {
|
|||||||
round
|
round
|
||||||
placeholder="请输入关键词采集信息"
|
placeholder="请输入关键词采集信息"
|
||||||
/>
|
/>
|
||||||
<n-button v-if="!running" type="primary" round size="large" @click="handleFetchInfoFromPage">
|
<n-button v-if="!isRunning" type="primary" round size="large" @click="handleStart">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
@ -113,7 +116,7 @@ const handleInterrupt = () => {
|
|||||||
中断
|
中断
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="running" class="running-tip-section">
|
<div v-if="isRunning" class="running-tip-section">
|
||||||
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
</div>
|
</div>
|
||||||
<progress-report class="progress-report" :timelines="timelines" />
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
import './main.css';
|
import './main.scss';
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('demo', () => {
|
|
||||||
it('should work', () => {
|
|
||||||
expect(1 + 1).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user