Refactor brand service and related components
- Removed ListQuestionVersions method and associated types from BrandService and Queries. - Updated PromptRuleGenerationService to change task naming and handling. - Enhanced ScheduleTaskService to support filtering by created date range and keyword. - Introduced InstantTaskService for managing instant tasks with appropriate filtering and response structures. - Added InstantTaskHandler for handling API requests related to instant tasks. - Created InstantTaskTab component for the admin web interface to display and manage instant tasks. - Updated database migrations to rename source types for articles and generation tasks. - Adjusted models and repository queries to reflect the removal of question version handling.
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FileSearchOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
@@ -13,6 +12,7 @@ import { computed, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import { brandsApi } from "@/lib/api";
|
||||
import { formatDateTime } from "@/lib/display";
|
||||
import { formatError } from "@/lib/errors";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
@@ -21,8 +21,6 @@ const { t } = useI18n();
|
||||
const activeTab = ref("keywords");
|
||||
const selectedBrandId = ref<number | null>(null);
|
||||
const selectedKeywordId = ref<number | null>(null);
|
||||
const versionsDrawerOpen = ref(false);
|
||||
const selectedQuestionId = ref<number | null>(null);
|
||||
|
||||
const brandModalOpen = ref(false);
|
||||
const editingBrandId = ref<number | null>(null);
|
||||
@@ -78,12 +76,6 @@ const filteredQuestionsQuery = useQuery({
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number, selectedKeywordId.value),
|
||||
});
|
||||
|
||||
const questionVersionsQuery = useQuery({
|
||||
queryKey: computed(() => ["brands", selectedBrandId.value, "question-versions"]),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
queryFn: () => brandsApi.listQuestionVersions(selectedBrandId.value as number),
|
||||
});
|
||||
|
||||
const competitorsQuery = useQuery({
|
||||
queryKey: computed(() => ["brands", selectedBrandId.value, "competitors"]),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
@@ -192,10 +184,7 @@ const questionMutations = {
|
||||
onSuccess: async () => {
|
||||
message.success(t("brands.messages.createQuestion"));
|
||||
questionModalOpen.value = false;
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "question-versions"] }),
|
||||
]);
|
||||
await queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] });
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
}),
|
||||
@@ -207,10 +196,7 @@ const questionMutations = {
|
||||
onSuccess: async () => {
|
||||
message.success(t("brands.messages.updateQuestion"));
|
||||
questionModalOpen.value = false;
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "question-versions"] }),
|
||||
]);
|
||||
await queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] });
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
}),
|
||||
@@ -219,10 +205,7 @@ const questionMutations = {
|
||||
brandsApi.removeQuestion(selectedBrandId.value as number, questionId),
|
||||
onSuccess: async () => {
|
||||
message.success(t("brands.messages.deleteQuestion"));
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "question-versions"] }),
|
||||
]);
|
||||
await queryClient.invalidateQueries({ queryKey: ["brands", selectedBrandId.value, "questions"] });
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
}),
|
||||
@@ -273,9 +256,6 @@ const competitorMutations = {
|
||||
const selectedKeyword = computed(() =>
|
||||
keywordsQuery.data.value?.find((item) => item.id === selectedKeywordId.value) ?? null,
|
||||
);
|
||||
const versionItems = computed(() =>
|
||||
(questionVersionsQuery.data.value || []).filter((item) => item.question_id === selectedQuestionId.value),
|
||||
);
|
||||
|
||||
const questionColumns = computed<TableColumnsType<Question>>(() => [
|
||||
{
|
||||
@@ -287,12 +267,13 @@ const questionColumns = computed<TableColumnsType<Question>>(() => [
|
||||
title: t("common.createdAt"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 160,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: t("common.actions"),
|
||||
key: "actions",
|
||||
width: 180,
|
||||
width: 120,
|
||||
align: "right",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -379,11 +360,6 @@ function openCompetitorModal(competitor?: Competitor): void {
|
||||
competitorModalOpen.value = true;
|
||||
}
|
||||
|
||||
function openVersions(questionId: number): void {
|
||||
selectedQuestionId.value = questionId;
|
||||
versionsDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitBrand(): Promise<void> {
|
||||
if (!brandForm.name.trim()) {
|
||||
return;
|
||||
@@ -474,13 +450,28 @@ async function submitCompetitor(): Promise<void> {
|
||||
</div>
|
||||
|
||||
<div class="brands-view__brand-card-actions">
|
||||
<a-button size="small" @click.stop="openBrandModal(brand)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
<a-tooltip :title="t('common.edit')">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-edit"
|
||||
@click.stop="openBrandModal(brand)"
|
||||
>
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm @confirm="brandMutations.remove.mutate(brand.id)">
|
||||
<template #title>{{ t("brands.deleteBrand") }}</template>
|
||||
<a-button size="small" danger @click.stop>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-delete"
|
||||
:title="t('common.delete')"
|
||||
@click.stop
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
@@ -497,7 +488,6 @@ async function submitCompetitor(): Promise<void> {
|
||||
<aside class="brands-view__keywords-panel">
|
||||
<div class="brands-view__section-head brands-view__section-head--compact">
|
||||
<div>
|
||||
<p class="eyebrow">{{ t("brands.sections.keywords") }}</p>
|
||||
<h3>{{ t("brands.sections.keywords") }}</h3>
|
||||
</div>
|
||||
<a-button type="primary" size="small" @click="openKeywordModal()">
|
||||
@@ -516,13 +506,28 @@ async function submitCompetitor(): Promise<void> {
|
||||
>
|
||||
<span>{{ keyword.name }}</span>
|
||||
<div class="brands-view__keyword-actions">
|
||||
<a-button size="small" @click.stop="openKeywordModal(keyword)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
<a-tooltip :title="t('common.edit')">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-edit"
|
||||
@click.stop="openKeywordModal(keyword)"
|
||||
>
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm @confirm="keywordMutations.remove.mutate(keyword.id)">
|
||||
<template #title>{{ t("common.delete") }}</template>
|
||||
<a-button size="small" danger @click.stop>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-delete"
|
||||
:title="t('common.delete')"
|
||||
@click.stop
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
@@ -554,24 +559,34 @@ async function submitCompetitor(): Promise<void> {
|
||||
<template v-if="column.key === 'question_text'">
|
||||
<div class="brands-view__question-cell">
|
||||
<strong>{{ record.question_text }}</strong>
|
||||
<span>{{ record.version_no ? `v${record.version_no}` : "--" }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
{{ record.created_at }}
|
||||
<span class="brands-view__datetime">{{ formatDateTime(record.created_at) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<div class="brands-view__inline-actions">
|
||||
<a-button size="small" @click="openVersions(record.id)">
|
||||
<template #icon><FileSearchOutlined /></template>
|
||||
</a-button>
|
||||
<a-button size="small" @click="openQuestionModal(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
<div class="table-actions-row">
|
||||
<a-tooltip :title="t('common.edit')">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-edit"
|
||||
@click="openQuestionModal(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm @confirm="questionMutations.remove.mutate(record.id)">
|
||||
<template #title>{{ t("common.delete") }}</template>
|
||||
<a-button size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-delete"
|
||||
:title="t('common.delete')"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
@@ -608,13 +623,27 @@ async function submitCompetitor(): Promise<void> {
|
||||
</a>
|
||||
</div>
|
||||
<div class="brands-view__inline-actions">
|
||||
<a-button size="small" @click="openCompetitorModal(competitor)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
<a-tooltip :title="t('common.edit')">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-edit"
|
||||
@click="openCompetitorModal(competitor)"
|
||||
>
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popconfirm @confirm="competitorMutations.remove.mutate(competitor.id)">
|
||||
<template #title>{{ t("common.delete") }}</template>
|
||||
<a-button size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-delete"
|
||||
:title="t('common.delete')"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
@@ -641,6 +670,7 @@ async function submitCompetitor(): Promise<void> {
|
||||
<a-modal
|
||||
v-model:open="brandModalOpen"
|
||||
:title="editingBrandId ? t('brands.editBrand') : t('brands.newBrand')"
|
||||
centered
|
||||
@ok="submitBrand"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
@@ -659,6 +689,7 @@ async function submitCompetitor(): Promise<void> {
|
||||
<a-modal
|
||||
v-model:open="keywordModalOpen"
|
||||
:title="editingKeywordId ? t('common.edit') : t('brands.actions.newKeyword')"
|
||||
centered
|
||||
@ok="submitKeyword"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
@@ -671,6 +702,7 @@ async function submitCompetitor(): Promise<void> {
|
||||
<a-modal
|
||||
v-model:open="questionModalOpen"
|
||||
:title="editingQuestionId ? t('common.edit') : t('brands.actions.newQuestion')"
|
||||
centered
|
||||
@ok="submitQuestion"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
@@ -689,6 +721,7 @@ async function submitCompetitor(): Promise<void> {
|
||||
<a-modal
|
||||
v-model:open="competitorModalOpen"
|
||||
:title="editingCompetitorId ? t('common.edit') : t('brands.actions.newCompetitor')"
|
||||
centered
|
||||
@ok="submitCompetitor"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
@@ -709,22 +742,6 @@ async function submitCompetitor(): Promise<void> {
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="versionsDrawerOpen"
|
||||
:title="t('brands.sections.versions')"
|
||||
width="520"
|
||||
class="brands-view__versions-drawer"
|
||||
>
|
||||
<a-list :data-source="versionItems" item-layout="vertical">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta :title="`v${item.version_no}`" :description="item.created_at" />
|
||||
<p class="brands-view__version-text">{{ item.question_text }}</p>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -880,7 +897,8 @@ async function submitCompetitor(): Promise<void> {
|
||||
.brands-view__keyword-actions,
|
||||
.brands-view__inline-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.brands-view__keyword-layout {
|
||||
@@ -922,12 +940,10 @@ async function submitCompetitor(): Promise<void> {
|
||||
.brands-view__question-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.brands-view__question-cell span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
.brands-view__datetime {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.brands-view__competitor-grid {
|
||||
@@ -975,16 +991,6 @@ async function submitCompetitor(): Promise<void> {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.brands-view__version-text {
|
||||
margin: 0;
|
||||
color: #334155;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.brands-view__versions-drawer :deep(.ant-drawer-body) {
|
||||
padding: 18px 24px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.brands-view__brand-grid,
|
||||
.brands-view__keyword-layout,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import CustomArticleTab from "@/components/CustomArticleTab.vue";
|
||||
import InstantTaskTab from "@/components/InstantTaskTab.vue";
|
||||
import PromptRuleTab from "@/components/PromptRuleTab.vue";
|
||||
import ScheduleTaskTab from "@/components/ScheduleTaskTab.vue";
|
||||
import InstantGenerateModal from "@/components/InstantGenerateModal.vue";
|
||||
@@ -56,6 +57,9 @@ function openScheduleModal(): void {
|
||||
<a-tab-pane key="articles" :tab="t('custom.tabs.articles')">
|
||||
<CustomArticleTab />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="instantTasks" :tab="t('custom.tabs.instantTasks')">
|
||||
<InstantTaskTab />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="scheduleTasks" :tab="t('custom.tabs.scheduleTasks')">
|
||||
<ScheduleTaskTab />
|
||||
</a-tab-pane>
|
||||
|
||||
@@ -172,7 +172,8 @@ const articleColumns = computed<TableColumnsType<ArticleListItem>>(() => [
|
||||
{
|
||||
title: t("common.actions"),
|
||||
key: "actions",
|
||||
width: 180,
|
||||
width: 120,
|
||||
align: "right",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -447,24 +448,36 @@ async function handleDelete(articleId: number): Promise<void> {
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<div class="templates-view__actions">
|
||||
<div class="table-actions-row">
|
||||
<a-tooltip
|
||||
:title="canEditArticle(record.generate_status) ? t('templates.list.edit') : t('templates.list.editDisabled')"
|
||||
>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!canEditArticle(record.generate_status)"
|
||||
@click="openEditor(record)"
|
||||
>
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
<span>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-edit"
|
||||
:disabled="!canEditArticle(record.generate_status)"
|
||||
@click="openEditor(record)"
|
||||
>
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-popconfirm
|
||||
:title="t('templates.list.deleteConfirm')"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button size="small" danger :loading="deleteMutation.isPending.value">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="action-btn action-delete"
|
||||
:loading="deleteMutation.isPending.value"
|
||||
:title="t('common.delete')"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
@@ -624,11 +637,6 @@ async function handleDelete(articleId: number): Promise<void> {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.templates-view__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.templates-view__status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user