11
This commit is contained in:
@@ -4,8 +4,12 @@ import { storeToRefs } from "pinia";
|
||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||
|
||||
const workflow = useWorkflowStore();
|
||||
const { ipBrainModalVisible, ipBrainProfileUrl, ipBrainCollecting } =
|
||||
storeToRefs(workflow);
|
||||
const {
|
||||
ipBrainModalVisible,
|
||||
ipBrainProfileUrl,
|
||||
ipBrainCollecting,
|
||||
ipBrainCollectProgress,
|
||||
} = storeToRefs(workflow);
|
||||
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
@@ -13,10 +17,11 @@ const tipItems = [
|
||||
"输入账号主页或分享链接后点击「开始采集」按钮",
|
||||
"系统会自动打开浏览器并抓取数据,请保持网络连接正常",
|
||||
"无需任何手动操作,等待采集完成即可",
|
||||
"采集完成后会自动将 5 个视频标题添加到选题库中",
|
||||
"采集完成后会自动将最多 6 个视频标题添加到选题库中",
|
||||
];
|
||||
|
||||
function onCancel() {
|
||||
if (ipBrainCollecting.value) return;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
workflow.closeIpBrainDialog();
|
||||
}
|
||||
@@ -24,7 +29,9 @@ function onCancel() {
|
||||
async function onStart() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const result = await workflow.startIpBrainCollect();
|
||||
if (!result.ok) {
|
||||
if (result.ok) {
|
||||
feedback.value = { severity: "success", message: result.message };
|
||||
} else {
|
||||
feedback.value = { severity: "error", message: result.message };
|
||||
}
|
||||
}
|
||||
@@ -38,10 +45,11 @@ async function onStart() {
|
||||
:style="{ width: '600px' }"
|
||||
:draggable="false"
|
||||
class="ip-brain-dialog"
|
||||
:closable="!ipBrainCollecting"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
v-if="feedback.message && !ipBrainCollecting"
|
||||
:severity="feedback.severity"
|
||||
:closable="false"
|
||||
class="w-full"
|
||||
@@ -49,6 +57,15 @@ async function onStart() {
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<Message
|
||||
v-if="ipBrainCollecting && ipBrainCollectProgress"
|
||||
severity="info"
|
||||
:closable="false"
|
||||
class="w-full"
|
||||
>
|
||||
{{ ipBrainCollectProgress }}
|
||||
</Message>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block font-medium text-slate-200">
|
||||
账号主页或分享链接
|
||||
@@ -64,7 +81,7 @@ async function onStart() {
|
||||
/>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
当前 IP 学习支持抖音账号主页、用户分享链接和短链接,程序会自动识别并采集最新
|
||||
5 个视频标题
|
||||
6 个视频标题
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
150
src/components/workflow/LegalCheckDialog.vue
Normal file
150
src/components/workflow/LegalCheckDialog.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||
|
||||
const emit = defineEmits(["feedback"]);
|
||||
|
||||
const workflow = useWorkflowStore();
|
||||
const { legalModalVisible, legalChecking, legalReport } = storeToRefs(workflow);
|
||||
|
||||
function onClose() {
|
||||
workflow.closeLegalModal();
|
||||
}
|
||||
|
||||
function onApplyFix() {
|
||||
emit("feedback", workflow.applyLegalFix());
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="legalModalVisible"
|
||||
modal
|
||||
header="AI 法务审核报告"
|
||||
:style="{ width: '600px' }"
|
||||
:draggable="false"
|
||||
class="legal-check-dialog"
|
||||
>
|
||||
<div v-if="legalChecking && !legalReport" class="py-10 text-center">
|
||||
<i class="pi pi-spin pi-spinner text-4xl text-blue-400" />
|
||||
<p class="mt-4 text-sm font-medium text-slate-200">AI 正在进行法务审核...</p>
|
||||
<p class="mt-1 text-xs text-slate-500">检查违禁词、敏感词、极限词等法律风险</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="legalReport" class="space-y-4">
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg border p-3"
|
||||
:class="
|
||||
legalReport.hasViolations
|
||||
? 'border-amber-700/40 bg-amber-900/20'
|
||||
: 'border-emerald-700/40 bg-emerald-900/20'
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="pi text-2xl"
|
||||
:class="
|
||||
legalReport.hasViolations
|
||||
? 'pi-exclamation-triangle text-amber-400'
|
||||
: 'pi-check-circle text-emerald-400'
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
class="text-base font-semibold"
|
||||
:class="legalReport.hasViolations ? 'text-amber-300' : 'text-emerald-300'"
|
||||
>
|
||||
{{ legalReport.hasViolations ? "发现风险" : "内容合规" }}
|
||||
</div>
|
||||
<div
|
||||
class="text-sm"
|
||||
:class="legalReport.hasViolations ? 'text-amber-400/80' : 'text-emerald-400/80'"
|
||||
>
|
||||
{{
|
||||
legalReport.hasViolations
|
||||
? `共发现 ${legalReport.totalCount} 处风险`
|
||||
: "文案内容符合规范,可以发布"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="legalReport.hasViolations">
|
||||
<div>
|
||||
<div class="mb-2 flex items-center gap-2 text-sm text-slate-400">
|
||||
<i class="pi pi-file" />
|
||||
<span>原文案分析</span>
|
||||
</div>
|
||||
<div
|
||||
class="legal-highlight-box max-h-36 overflow-y-auto rounded-lg border border-[#2a2a3a] bg-[#0d0d1a] p-3 text-sm leading-relaxed text-slate-300"
|
||||
v-html="legalReport.highlightedText"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="legalReport.fixedText">
|
||||
<div class="mb-2 flex items-center gap-2 text-sm text-emerald-400">
|
||||
<i class="pi pi-pencil" />
|
||||
<span>优化后文案</span>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-emerald-800/40 bg-emerald-950/30 p-3 text-sm leading-relaxed text-slate-200 whitespace-pre-wrap"
|
||||
>
|
||||
{{ legalReport.fixedText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="legalReport.analysis">
|
||||
<div class="mb-2 flex items-center gap-2 text-sm text-blue-400">
|
||||
<i class="pi pi-lightbulb" />
|
||||
<span>AI 审核解读</span>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-[#2a2a3a] bg-[#12121f] p-3 text-sm leading-relaxed text-slate-300 whitespace-pre-wrap"
|
||||
>
|
||||
{{ legalReport.analysis }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="legalReport.risks?.length">
|
||||
<div class="mb-2 flex items-center gap-2 text-sm text-red-400">
|
||||
<i class="pi pi-list" />
|
||||
<span>风险详情</span>
|
||||
</div>
|
||||
<div class="max-h-48 space-y-2 overflow-y-auto">
|
||||
<div
|
||||
v-for="(risk, idx) in legalReport.risks"
|
||||
:key="idx"
|
||||
class="rounded p-2 text-sm hover:bg-[#1a1a2e]"
|
||||
>
|
||||
<span class="font-medium text-red-300">{{ risk.word }}</span>
|
||||
<div class="mt-1 text-slate-400">
|
||||
<span class="text-slate-600">→</span>
|
||||
{{ risk.recommendation ? risk.recommendation : "建议删除" }}
|
||||
</div>
|
||||
<div v-if="risk.reason" class="mt-1 text-xs text-slate-500">
|
||||
{{ risk.reason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="关闭" severity="secondary" @click="onClose" />
|
||||
<Button
|
||||
v-if="legalReport?.hasViolations"
|
||||
label="采用优化文案"
|
||||
@click="onApplyFix"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.legal-highlight-box :deep(.legal-hit) {
|
||||
background: rgb(254 202 202);
|
||||
color: rgb(153 27 27);
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||
import AddIpBrainDialog from "./AddIpBrainDialog.vue";
|
||||
@@ -17,19 +18,51 @@ const {
|
||||
recognizedContent,
|
||||
ipBenchmarks,
|
||||
ipBrainSelectedBenchmarkId,
|
||||
ipTopics,
|
||||
ipBrainSelectedTopicId,
|
||||
ipTopicsRewriting,
|
||||
ipTopicRewriteProgress,
|
||||
rewrittenTopics,
|
||||
} = storeToRefs(workflow);
|
||||
|
||||
const topicFeedback = ref({ severity: "", message: "" });
|
||||
|
||||
function onSelectBenchmark(id) {
|
||||
topicFeedback.value = { severity: "", message: "" };
|
||||
workflow.selectBenchmark(id);
|
||||
}
|
||||
|
||||
function onSelectRewrittenTopic(topicId) {
|
||||
workflow.selectRewrittenTopic(topicId);
|
||||
}
|
||||
|
||||
async function onRefreshRewritten() {
|
||||
topicFeedback.value = { severity: "", message: "" };
|
||||
const result = await workflow.rewriteIpTopics();
|
||||
if (result.ok) {
|
||||
topicFeedback.value = { severity: "success", message: result.message };
|
||||
} else {
|
||||
topicFeedback.value = { severity: "error", message: result.message };
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveBenchmark(id, event) {
|
||||
event?.stopPropagation?.();
|
||||
workflow.removeBenchmark(id);
|
||||
}
|
||||
|
||||
function onOpenVideoLink() {
|
||||
workflow.videoLinkModalVisible = true;
|
||||
}
|
||||
|
||||
function onOpenIpBrain() {
|
||||
if (workflow.ipBenchmarkCount >= 5) return;
|
||||
workflow.ipBrainModalVisible = true;
|
||||
}
|
||||
|
||||
function topicCount(benchmark) {
|
||||
return benchmark?.topicLibrary?.length ?? 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,8 +81,12 @@ function onOpenIpBrain() {
|
||||
label="添加IP大脑"
|
||||
class="w-full"
|
||||
size="small"
|
||||
:disabled="workflow.ipBenchmarkCount >= 5"
|
||||
@click="onOpenIpBrain"
|
||||
/>
|
||||
<p v-if="workflow.ipBenchmarkCount >= 5" class="mt-1 text-center text-xs text-amber-400">
|
||||
已达上限(5/5)
|
||||
</p>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="dashboard-field-label">
|
||||
@@ -59,17 +96,32 @@ function onOpenIpBrain() {
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="ipBenchmarks.length === 0" class="dashboard-muted py-2">暂无对标</p>
|
||||
<div v-else class="py-1">
|
||||
<button
|
||||
<div v-else class="space-y-1 py-1">
|
||||
<div
|
||||
v-for="item in ipBenchmarks"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="benchmark-item"
|
||||
:class="{ 'benchmark-item--active': ipBrainSelectedBenchmarkId === item.id }"
|
||||
@click="onSelectBenchmark(item.id)"
|
||||
class="benchmark-row flex items-center justify-between gap-2"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="benchmark-item min-w-0 flex-1"
|
||||
:class="{ 'benchmark-item--active': ipBrainSelectedBenchmarkId === item.id }"
|
||||
@click="onSelectBenchmark(item.id)"
|
||||
>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<span class="ml-1 shrink-0 text-xs opacity-70">
|
||||
({{ topicCount(item) }}/6)
|
||||
</span>
|
||||
</button>
|
||||
<Button
|
||||
label="删除"
|
||||
size="small"
|
||||
severity="danger"
|
||||
text
|
||||
class="shrink-0"
|
||||
@click="onRemoveBenchmark(item.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,16 +133,72 @@ function onOpenIpBrain() {
|
||||
>
|
||||
请先选择一个对标
|
||||
</p>
|
||||
<p v-else-if="workflow.ipTopics.length === 0" class="dashboard-muted py-2 text-sm">
|
||||
<p v-else-if="ipTopics.length === 0" class="dashboard-muted py-2 text-sm">
|
||||
采集完成后将显示视频标题
|
||||
</p>
|
||||
<ul v-else class="space-y-1 py-1 text-sm text-slate-300">
|
||||
<li v-for="(topic, index) in workflow.ipTopics" :key="topic.id">
|
||||
{{ index + 1 }}. {{ topic.title }}
|
||||
<ul v-else class="space-y-1 py-1">
|
||||
<li
|
||||
v-for="(topic, index) in ipTopics"
|
||||
:key="topic.id"
|
||||
class="rounded-lg border border-white/5 bg-white/5 px-2 py-2 text-sm text-slate-400"
|
||||
>
|
||||
{{ index + 1 }}. {{ topic.original || topic.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<div class="dashboard-field-label">仿写后</div>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
:loading="ipTopicsRewriting"
|
||||
:disabled="!ipBrainSelectedBenchmarkId || ipTopics.length === 0 || ipTopicsRewriting"
|
||||
aria-label="刷新仿写标题"
|
||||
@click="onRefreshRewritten"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="ipTopicRewriteProgress" class="mb-2 text-xs text-blue-400">
|
||||
{{ ipTopicRewriteProgress }}
|
||||
</p>
|
||||
<p
|
||||
v-if="!ipBrainSelectedBenchmarkId"
|
||||
class="dashboard-muted py-2 text-center text-sm"
|
||||
>
|
||||
请先选择一个对标
|
||||
</p>
|
||||
<p
|
||||
v-else-if="rewrittenTopics.length === 0"
|
||||
class="dashboard-muted py-2 text-sm"
|
||||
>
|
||||
点击右侧刷新按钮,根据选题库一键生成仿写标题
|
||||
</p>
|
||||
<ul v-else class="space-y-1 py-1">
|
||||
<li
|
||||
v-for="(topic, index) in rewrittenTopics"
|
||||
:key="topic.id"
|
||||
class="topic-item cursor-pointer rounded-lg border px-2 py-2 text-sm transition-colors"
|
||||
:class="
|
||||
ipBrainSelectedTopicId === topic.id
|
||||
? 'border-blue-400/50 bg-blue-500/20 text-slate-100 ring-1 ring-blue-400/40'
|
||||
: 'border-white/5 bg-white/5 text-slate-300 hover:bg-white/10'
|
||||
"
|
||||
@click="onSelectRewrittenTopic(topic.id)"
|
||||
>
|
||||
{{ index + 1 }}. {{ topic.rewritten }}
|
||||
</li>
|
||||
</ul>
|
||||
<p
|
||||
v-if="topicFeedback.message"
|
||||
class="mt-2 text-sm"
|
||||
:class="topicFeedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
||||
>
|
||||
{{ topicFeedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AddIpBrainDialog />
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,21 +2,39 @@
|
||||
import { ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||
import LegalCheckDialog from "./LegalCheckDialog.vue";
|
||||
|
||||
const workflow = useWorkflowStore();
|
||||
const { scriptContent, editLanguage, editWordCount, scriptGenerating } =
|
||||
storeToRefs(workflow);
|
||||
const {
|
||||
scriptContent,
|
||||
editLanguage,
|
||||
editWordCount,
|
||||
scriptGenerating,
|
||||
legalChecking,
|
||||
} = storeToRefs(workflow);
|
||||
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
function showFeedback(result) {
|
||||
if (!result?.message) return;
|
||||
feedback.value = {
|
||||
severity: result.ok ? (result.warn ? "warn" : "success") : "error",
|
||||
message: result.message,
|
||||
};
|
||||
}
|
||||
|
||||
async function onGenerateScript() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const result = await workflow.generateScript();
|
||||
if (result.ok) {
|
||||
feedback.value = { severity: "success", message: result.message };
|
||||
} else {
|
||||
feedback.value = { severity: "error", message: result.message };
|
||||
}
|
||||
showFeedback(await workflow.generateScript());
|
||||
}
|
||||
|
||||
async function onLegalCheck() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
showFeedback(await workflow.runLegalCheck());
|
||||
}
|
||||
|
||||
function onLegalFeedback(result) {
|
||||
showFeedback(result);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -53,18 +71,31 @@ async function onGenerateScript() {
|
||||
label="撰写文案"
|
||||
size="small"
|
||||
:loading="scriptGenerating"
|
||||
:disabled="scriptGenerating"
|
||||
:disabled="scriptGenerating || legalChecking"
|
||||
@click="onGenerateScript"
|
||||
/>
|
||||
<Button label="AI法务" size="small" severity="secondary" />
|
||||
<Button
|
||||
label="AI法务"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
:loading="legalChecking"
|
||||
:disabled="scriptGenerating || legalChecking"
|
||||
@click="onLegalCheck"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="feedback.message"
|
||||
class="text-sm"
|
||||
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
||||
:class="{
|
||||
'text-red-400': feedback.severity === 'error',
|
||||
'text-amber-400': feedback.severity === 'warn',
|
||||
'text-emerald-400': feedback.severity === 'success',
|
||||
}"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LegalCheckDialog @feedback="onLegalFeedback" />
|
||||
</DashboardCard>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user