This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 09:45:42 +08:00
parent 27e83c564c
commit 8007262e8f
8 changed files with 742 additions and 93 deletions

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>