feat: add MulInputModal

This commit is contained in:
PetrichorFun 2026-01-21 14:59:05 +08:00
parent 18ad1332db
commit 3a8110171f
7 changed files with 271 additions and 29 deletions

View File

@ -1,9 +1,9 @@
{
"name": "azon-seeker",
"displayName": "Azon Seeker",
"version": "0.7.1",
"displayName": "Azon Seeker v0.7.1.1-beta",
"version": "0.7.2",
"private": true,
"description": "Starter modify by honestfox101",
"description": "Starter modify by honestfox101 and PetrichorFun",
"scripts": {
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:prepare dev:web dev:background dev:js",
"dev-firefox": "npm run clear-firefox && cross-env NODE_ENV=development EXTENSION=firefox run-p dev:prepare dev:web dev:background dev:js",

View File

@ -0,0 +1,77 @@
# MulInputModal.vue 修改计划
## 目标
修改 `src/sidepanel/views/components/MulInputModal.vue` 组件,使其支持从 Excel 复制一列值粘贴到输入框,并提供格式验证和确认后的消息通知。
## 需求详情
1. **输入格式**:每行一个值(换行分隔),不允许空行。
2. **实时验证**:在用户输入时检查格式是否正确,并给出提示。
3. **确认通知**:点击确认按钮后,显示 n-message 通知成功或失败。
## 当前组件分析
当前组件是一个简单的模态对话框,包含一个文本区域输入框。它通过 `v-model:show-modal` 控制显示,但没有处理输入值或验证逻辑。
## 设计修改
### 1. 模板修改
- 保留现有的 `n-modal``n-dialog` 结构。
- 修改 `n-dialog` 属性:
- 移除 `content` 属性(因为其内容与功能不符)。
- 动态设置确认按钮的文本和状态:格式正确时显示“确认”,格式错误时显示“格式错误”并禁用按钮。
- 增强 `n-input`
- 添加 `placeholder` 提示用户粘贴格式。
- 设置 `autosize` 适应多行内容。
- 绑定 `v-model:value` 到本地输入文本。
- 添加验证提示区域:
- 显示验证状态消息(成功/错误)。
- 显示行数统计。
### 2. 脚本修改
- 导入必要的 Vue 和 Naive UI API。
- 定义模型:
- `showModal`:控制模态框显示。
- `modelValue`:可选,用于双向绑定关键词数组(字符串数组)。
- 定义本地响应式数据:
- `inputText`:文本区域的绑定值。
- 计算属性 `validation`
- 根据输入文本实时验证格式。
- 返回 `valid``lines`(非空行数组)、`message`
- 验证规则:
1. 输入不能为空(去除首尾空格后)。
2. 不允许空行(任何一行 trim 后为空即无效)。
3. 检测制表符(警告用户可能粘贴了多列)。
- 确认处理函数 `handleConfirm`
- 检查验证结果,如果无效则显示错误消息并返回。
- 如果有效,更新 `modelValue`,发出 `confirm` 事件,显示成功消息,关闭模态框。
- 初始化:当 `modelValue` 变化时,将数组转换为换行分隔的文本并填充到输入框。
### 3. 样式修改
- 添加验证提示区域的样式,根据状态改变颜色。
- 调整布局间距。
## 代码草案
详见 [MulInputModal.vue 草案](草案内容略)
## 向后兼容性
- 添加的 `modelValue` 模型为可选,不影响现有使用(父组件可以不传递)。
- 现有的 `showModal` 模型保持不变。
- 新增的 `confirm` 事件可选,父组件可以监听以获取导入的行。
## 测试计划
1. 手动测试从 Excel 复制一列值并粘贴到输入框。
2. 验证实时提示是否正确显示。
3. 测试确认按钮在格式正确/错误时的行为。
4. 测试 n-message 通知是否正常弹出。
## 下一步
完成详细设计后,切换到 Code 模式实施修改。

11
pnpm-lock.yaml generated
View File

@ -988,7 +988,6 @@ packages:
}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.45.1':
resolution:
@ -997,7 +996,6 @@ packages:
}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.45.1':
resolution:
@ -1006,7 +1004,6 @@ packages:
}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.45.1':
resolution:
@ -1015,7 +1012,6 @@ packages:
}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.45.1':
resolution:
@ -1024,7 +1020,6 @@ packages:
}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.45.1':
resolution:
@ -1033,7 +1028,6 @@ packages:
}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.45.1':
resolution:
@ -1042,7 +1036,6 @@ packages:
}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.45.1':
resolution:
@ -1051,7 +1044,6 @@ packages:
}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.45.1':
resolution:
@ -1060,7 +1052,6 @@ packages:
}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.45.1':
resolution:
@ -1069,7 +1060,6 @@ packages:
}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.45.1':
resolution:
@ -1078,7 +1068,6 @@ packages:
}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.45.1':
resolution:

View File

@ -2,6 +2,9 @@
const openOptionsPage = async () => {
await browser.runtime.openOptionsPage();
};
const emit = defineEmits<{
(e: 'clickMulInput', event: MouseEvent): void;
}>();
</script>
<template>
@ -15,10 +18,20 @@ const openOptionsPage = async () => {
</template>
<template #default>数据</template>
</n-button>
<n-button class="setting-button" round @click="$emit('clickMulInput', $el)" size="small">
<template #icon>
<n-icon size="18" color="#0f0f0f">
<stash:list-add />
</n-icon>
</template>
<template #default>输入</template>
</n-button>
</div>
<n-space class="app-title">
<mdi-cat style="font-size: 60px; color: black" />
<h1><slot></slot></h1>
<h1>
<slot></slot>
</h1>
</n-space>
</div>
</template>
@ -40,6 +53,7 @@ const openOptionsPage = async () => {
.setting-button {
margin: 12px 20px 0 0;
opacity: 0.7;
&:hover {
opacity: 1;
}

View File

@ -14,5 +14,7 @@ export function isForbiddenUrl(url: string): boolean {
export const isFirefox = navigator.userAgent.includes('Firefox');
export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000';
// export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000';
export const remoteHost = __DEV__ ? '127.0.0.1:18000' : 'vm8nc3zr-18000.usw2.devtunnels.ms';
// export const remoteHost = '47.251.4.191:8000';

View File

@ -2,8 +2,9 @@
import { keywordsList } from '~/storages/amazon';
import type { Timeline } from '~/components/ProgressReport.vue';
import { usePageWorker } from '~/page-worker';
import MulInputModal from '~/sidepanel/views/components/MulInputModal.vue';
const message = useMessage();
const showModal = ref(false);
//#region Initial Page Worker
const worker = usePageWorker('amazon', { objects: ['search'] });
@ -72,23 +73,35 @@ const handleInterrupt = () => {
worker.stop();
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
};
const clickInputButton = (e: MouseEvent) => {
showModal.value = true;
};
const handleModalConfirm = (keys: string[]) => {
keywordsList.value = keys;
// console.log(keys);
};
</script>
<template>
<div class="search-page-entry">
<header-title>Amazon Search</header-title>
<header-title @click-mul-input="clickInputButton">Amazon Search</header-title>
<div class="interactive-section">
<n-dynamic-input
:disabled="worker.isRunning.value"
v-model:value="keywordsList"
:min="1"
:max="10"
class="search-input-box"
autosize
size="large"
round
placeholder="请输入关键词采集信息"
/>
<n-infinite-scroll style="max-height: 60vh; padding-right: 16px" :distance="10">
<n-dynamic-input
:disabled="worker.isRunning.value"
v-model:value="keywordsList"
:min="1"
:max="999"
class="search-input-box"
autosize
size="large"
round
placeholder="请输入关键词采集信息"
/>
</n-infinite-scroll>
<!-- <n-dynamic-input :disabled="worker.isRunning.value" v-model:value="keywordsList" :min="1" :max="999"
class="search-input-box" autosize size="large" round placeholder="请输入关键词采集信息" /> -->
<n-button
v-if="!worker.isRunning.value"
type="primary"
@ -117,6 +130,7 @@ const handleInterrupt = () => {
</div>
<progress-report class="progress-report" :timelines="timelines" />
</div>
<MulInputModal v-model:show-modal="showModal" @confirm="handleModalConfirm"> </MulInputModal>
</template>
<style scoped lang="scss">

View File

@ -0,0 +1,146 @@
<template>
<n-modal
style="width: 80vw"
title="确认"
positive-text="确认"
negative-text="算了"
v-model:show="showModal"
>
<n-dialog
title="输入关键词"
:positive-text="validation.valid ? '确认' : '格式错误'"
negative-text="取消"
@positive-click="handleConfirm"
@negative-click="handleCancel"
:positive-button-props="{
type: validation.valid ? 'primary' : 'error',
disabled: !validation.valid,
}"
@close="handleCancel"
>
<n-input
v-model:value="inputText"
type="textarea"
placeholder="请从 Excel 复制一列值粘贴到这里,每行一个关键词,不允许空行"
:autosize="{ minRows: 8, maxRows: 20 }"
@input="handleInput"
/>
<div class="validation-hint" :class="{ valid: validation.valid, invalid: !validation.valid }">
<n-text :type="validation.valid ? 'success' : 'error'">
{{ validation.message }}
</n-text>
<n-text v-if="validation.valid" depth="3" class="line-count">
{{ validation.lines.length }}
</n-text>
</div>
</n-dialog>
</n-modal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useMessage } from 'naive-ui';
const message = useMessage();
const showModal = defineModel<boolean>('showModal', { default: false });
const props = defineProps<{
initialValue?: string[];
}>();
const inputText = ref('');
//
const validation = computed(() => {
const text = inputText.value;
const lines = text.split('\n').map((line) => line.trim());
const nonEmptyLines = lines.filter((line) => line.length > 0);
if (text.trim() === '') {
return { valid: false, lines: [], message: '输入不能为空' };
}
// trim
const hasEmptyLine = lines.some((line) => line.length === 0);
if (hasEmptyLine) {
return { valid: false, lines: [], message: '存在空行,请删除空行' };
}
//
const hasTab = lines.some((line) => line.includes('\t'));
if (hasTab) {
return { valid: false, lines: [], message: '检测到制表符,请确保每行只有一个值' };
}
return {
valid: true,
lines: nonEmptyLines,
message: '格式正确',
};
});
const handleInput = () => {
//
};
const handleConfirm = () => {
if (!validation.value.valid) {
message.error('格式错误:' + validation.value.message);
return;
}
const lines = validation.value.lines;
//
emit('confirm', lines);
//
message.success(`成功导入 ${lines.length} 个关键词`);
showModal.value = false;
};
const handleCancel = () => {
showModal.value = false;
};
const emit = defineEmits<{
confirm: [lines: string[]];
}>();
watch(
showModal,
(newVal) => {
if (!newVal) {
//
inputText.value = '';
} else {
//
if (props.initialValue && props.initialValue.length > 0) {
inputText.value = props.initialValue.join('\n');
} else {
inputText.value = '';
}
}
},
{ immediate: true },
);
</script>
<style scoped>
.validation-hint {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.validation-hint.valid {
color: var(--n-color-success);
}
.validation-hint.invalid {
color: var(--n-color-error);
}
.line-count {
font-size: 12px;
}
</style>