feat(patrol-delete): 删除条件改为添加式交互——输入后回车/点添加逐个加入chips列表(可单项移除),去掉原'保存到条件库+勾选'两步式与已知状态下拉,启动时随任务携带条件

This commit is contained in:
2026-09-09 19:06:27 +08:00
parent 96e93944d9
commit 6fe67b648f
@@ -63,51 +63,43 @@
<div class="section-title condition-title"> <div class="section-title condition-title">
<span>删除条件</span> <span>删除条件</span>
<span class="condition-title-hint"> <span class="condition-title-hint">
注意删除条件的描述需要和页面描述一致 输入删除条件后点击添加或回车启动时作为本次任务的删除条件携带
</span> </span>
</div> </div>
<div class="condition-panel"> <div class="condition-panel">
<div class="condition-input-row"> <div class="condition-input-row">
<el-input <el-input
v-model="conditionInput" v-model="conditionSelectValue"
clearable clearable
placeholder="请输入删除条件" placeholder="请输入删除条件,回车或点击添加"
@keyup.enter="confirmAddCondition" @keyup.enter="addCurrentCondition"
/> />
<button <button
type="button" type="button"
class="opt-btn" class="opt-btn"
:disabled="addingCondition" @click="addCurrentCondition"
@click="confirmAddCondition"
> >
{{ addingCondition ? "保存中..." : "保存" }} 添加
</button> </button>
</div> </div>
<div v-if="!deleteConditions.length" class="empty-conditions"> <div v-if="!taskConditionTexts.length" class="empty-conditions">
暂无删除条件保存后可在启动任务时多选携带 暂无删除条件输入后点击添加
</div> </div>
<ul v-else class="condition-list"> <ul v-else class="condition-list condition-list--chips">
<li <li
v-for="condition in deleteConditions" v-for="text in taskConditionTexts"
:key="condition.id" :key="text"
class="condition-item" class="condition-chip"
:title="text"
> >
<label class="condition-check"> <span class="condition-chip-text">{{ text }}</span>
<input
v-model="selectedConditionIds"
type="checkbox"
:value="condition.id"
/>
<span :title="condition.conditionText">
{{ condition.conditionText }}
</span>
</label>
<button <button
type="button" type="button"
class="link-danger" class="condition-chip-remove"
@click="removeCondition(condition.id)" title="移除"
@click="removeTaskConditionText(text)"
> >
删除 ×
</button> </button>
</li> </li>
</ul> </ul>
@@ -247,22 +239,18 @@ import TaskCenterPanel from "@/shared/components/tasks/TaskCenterPanel.vue";
import type { TaskItemView, TaskStatCard } from "@/shared/components/tasks/types"; import type { TaskItemView, TaskStatCard } from "@/shared/components/tasks/types";
import { import {
addPatrolDeleteCandidate, addPatrolDeleteCandidate,
addPatrolDeleteCondition,
createPatrolDeleteTask, createPatrolDeleteTask,
deletePatrolDeleteCandidate, deletePatrolDeleteCandidate,
deletePatrolDeleteCondition,
deletePatrolDeleteHistory, deletePatrolDeleteHistory,
deletePatrolDeleteTask, deletePatrolDeleteTask,
getPatrolDeleteDashboard, getPatrolDeleteDashboard,
getPatrolDeleteHistory, getPatrolDeleteHistory,
getPatrolDeleteTaskProgressBatch, getPatrolDeleteTaskProgressBatch,
getPatrolDeleteResultDownloadUrl, getPatrolDeleteResultDownloadUrl,
listPatrolDeleteConditions,
listPatrolDeleteCandidates, listPatrolDeleteCandidates,
matchPatrolDeleteShops, matchPatrolDeleteShops,
submitPatrolDeleteTaskResult, submitPatrolDeleteTaskResult,
type PatrolDeleteCandidateVo, type PatrolDeleteCandidateVo,
type PatrolDeleteConditionVo,
type PatrolDeleteCartRatio, type PatrolDeleteCartRatio,
type PatrolDeleteCountrySection, type PatrolDeleteCountrySection,
type PatrolDeleteDashboardVo, type PatrolDeleteDashboardVo,
@@ -289,11 +277,12 @@ const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion(); const ziniaoVersion = useZiniaoVersion();
const shopInput = ref(""); const shopInput = ref("");
const conditionInput = ref("");
const candidates = ref<PatrolDeleteCandidateVo[]>([]); const candidates = ref<PatrolDeleteCandidateVo[]>([]);
const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]); const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]);
const deleteConditions = ref<PatrolDeleteConditionVo[]>([]); /** 本次任务已添加的删除条件(文本;输入后回车/点添加逐个加入,启动时随任务携带) */
const selectedConditionIds = ref<number[]>([]); const taskConditionTexts = ref<string[]>([]);
/** 删除条件输入框当前值 */
const conditionSelectValue = ref("");
const selectedCountryCodes = ref<string[]>([...EU_COUNTRY_CODES]); const selectedCountryCodes = ref<string[]>([...EU_COUNTRY_CODES]);
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]); const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
const historyItems = ref<PatrolDeleteHistoryItem[]>([]); const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
@@ -304,7 +293,6 @@ const dashboard = ref<PatrolDeleteDashboardVo>({
failedTaskCount: 0, failedTaskCount: 0,
}); });
const adding = ref(false); const adding = ref(false);
const addingCondition = ref(false);
const matching = ref(false); const matching = ref(false);
const pushing = ref(false); const pushing = ref(false);
const queuePushResult = ref(""); const queuePushResult = ref("");
@@ -731,14 +719,6 @@ async function loadCandidates() {
candidates.value = await listPatrolDeleteCandidates(); candidates.value = await listPatrolDeleteCandidates();
} }
async function loadConditions() {
deleteConditions.value = await listPatrolDeleteConditions();
const existingIds = new Set(deleteConditions.value.map((item) => item.id));
selectedConditionIds.value = selectedConditionIds.value.filter((id) =>
existingIds.has(id),
);
}
async function loadDashboard() { async function loadDashboard() {
dashboard.value = await getPatrolDeleteDashboard(); dashboard.value = await getPatrolDeleteDashboard();
} }
@@ -848,39 +828,27 @@ async function confirmAdd() {
} }
} }
async function confirmAddCondition() { function addConditionText(text: string) {
const text = conditionInput.value.trim(); const trimmed = String(text || "").trim();
if (!text) { if (!trimmed) {
ElMessage.warning("请输入删除条件"); ElMessage.warning("请输入删除条件");
return; return;
} }
addingCondition.value = true; if (taskConditionTexts.value.includes(trimmed)) {
try { ElMessage.warning(`删除条件「${trimmed}」已添加`);
const saved = await addPatrolDeleteCondition(text); return;
conditionInput.value = "";
await loadConditions();
if (saved?.id && !selectedConditionIds.value.includes(saved.id)) {
selectedConditionIds.value = [...selectedConditionIds.value, saved.id];
}
ElMessage.success("删除条件已保存");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "保存失败");
} finally {
addingCondition.value = false;
} }
taskConditionTexts.value = [...taskConditionTexts.value, trimmed];
} }
async function removeCondition(id: number) { /** 「添加」按钮 / 回车:把输入文本加入本次任务条件列表 */
try { function addCurrentCondition() {
await deletePatrolDeleteCondition(id); addConditionText(conditionSelectValue.value);
selectedConditionIds.value = selectedConditionIds.value.filter( conditionSelectValue.value = "";
(item) => item !== id, }
);
await loadConditions(); function removeTaskConditionText(text: string) {
ElMessage.success("删除条件已删除"); taskConditionTexts.value = taskConditionTexts.value.filter((item) => item !== text);
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "删除失败");
}
} }
async function removeCandidate(id: number) { async function removeCandidate(id: number) {
@@ -947,12 +915,9 @@ function buildTaskItem(item: PatrolDeleteShopQueueItem): PatrolDeleteTaskItem {
} }
function selectedDeleteConditions() { function selectedDeleteConditions() {
const selected = new Set(selectedConditionIds.value); return taskConditionTexts.value.map((text) => ({
return deleteConditions.value id: text,
.filter((condition) => selected.has(condition.id)) conditionText: text,
.map((condition) => ({
id: condition.id,
conditionText: condition.conditionText,
})); }));
} }
@@ -1223,7 +1188,7 @@ onMounted(async () => {
loadQueueState(); loadQueueState();
loadCountryCodes(); loadCountryCodes();
watch(selectedCountryCodes, saveCountryCodes, { deep: true }); watch(selectedCountryCodes, saveCountryCodes, { deep: true });
await Promise.all([loadCandidates(), loadConditions(), loadDashboard(), loadHistory()]); await Promise.all([loadCandidates(), loadDashboard(), loadHistory()]);
if (activeTaskId.value) { if (activeTaskId.value) {
queuePushResult.value = queuePushResult.value =
@@ -1285,11 +1250,11 @@ async function batchDeleteHistory(views: TaskItemView[]) {
.condition-input-row :deep(.el-input) { flex: 1; } .condition-input-row :deep(.el-input) { flex: 1; }
.empty-conditions { color: #5e6878; font-size: 12px; padding: 10px 4px; } .empty-conditions { color: #5e6878; font-size: 12px; padding: 10px 4px; }
.condition-list { list-style: none; margin: 0; padding: 0; max-height: 150px; overflow: auto; display: flex; flex-direction: column; gap: 8px; } .condition-list { list-style: none; margin: 0; padding: 0; max-height: 150px; overflow: auto; display: flex; flex-direction: column; gap: 8px; }
.condition-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 6px; border-bottom: 1px solid #333f55; } .condition-list--chips { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; max-height: none; }
.condition-item:last-child { border-bottom: none; } .condition-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 6px 4px 10px; border: 1px solid #3e6a8f; border-radius: 999px; background: #232a3b; color: #cfd6df; font-size: 12px; }
.condition-check { display: flex; align-items: center; gap: 8px; min-width: 0; color: #cfd6df; font-size: 12px; cursor: pointer; } .condition-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; }
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; } .condition-chip-remove { border: none; background: transparent; color: #8ba3c0; cursor: pointer; font-size: 14px; line-height: 1; padding: 0 4px; }
.condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .condition-chip-remove:hover { color: #f87171; }
.link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; } .link-danger { background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 12px; padding: 0; }
.link-danger:hover { text-decoration: underline; } .link-danger:hover { text-decoration: underline; }
.run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; } .run-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }