Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
M
mengchangaigc
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姚珂
mengchangaigc
Commits
048729dc
Commit
048729dc
authored
Sep 01, 2026
by
黄同智
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
基本完成agent 创作的模块内容
parent
4ebe9c71
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
2266 additions
and
23 deletions
+2266
-23
index.ts
apps/web-vue/src/router/index.ts
+6
-0
AgentHistoryDrawer.vue
...web-vue/src/views/agent/components/AgentHistoryDrawer.vue
+390
-0
Step1.vue
apps/web-vue/src/views/agent/components/Step1.vue
+269
-0
Step2.vue
apps/web-vue/src/views/agent/components/Step2.vue
+393
-0
Step2SubjectDialog.vue
...web-vue/src/views/agent/components/Step2SubjectDialog.vue
+346
-0
Step3.vue
apps/web-vue/src/views/agent/components/Step3.vue
+373
-0
Step4.vue
apps/web-vue/src/views/agent/components/Step4.vue
+171
-0
index.vue
apps/web-vue/src/views/agent/index.vue
+28
-23
product-video.vue
apps/web-vue/src/views/agent/product-video.vue
+290
-0
No files found.
apps/web-vue/src/router/index.ts
View file @
048729dc
...
...
@@ -82,6 +82,12 @@ const routes = [
name
:
'Agent'
,
component
:
Agent
,
meta
:
{
title
:
'Agent创作'
,
icon
:
'audio'
,
affix
:
true
}
},
{
path
:
'product-video'
,
name
:
'AgentProductVideo'
,
component
:
()
=>
import
(
'@/views/agent/product-video.vue'
),
meta
:
{
title
:
'商品营销视频'
,
hidden
:
true
}
}
]
},
...
...
apps/web-vue/src/views/agent/components/AgentHistoryDrawer.vue
0 → 100644
View file @
048729dc
<
template
>
<el-drawer
v-model=
"drawerVisible"
direction=
"rtl"
size=
"388px"
:with-header=
"false"
:modal=
"false"
class=
"history-drawer"
>
<div
class=
"task-drawer h-full flex-col bg-fff"
>
<header
class=
"drawer-header flex-between h-56 px-18"
>
<div
class=
"flex-items-center gap-8"
>
<el-icon
class=
"fs-18 color-main"
>
<Refresh
/>
</el-icon>
<span
class=
"fs-16 fw-700 color-000"
>
任务队列
</span>
<span
class=
"count-tag h-20 lh-20 px-8 round-4 fs-12 color-888 fw-700"
>
30
</span>
</div>
<el-icon
class=
"fs-18 color-888 pointer"
@
click=
"drawerVisible = false"
>
<Close
/>
</el-icon>
</header>
<div
class=
"task-stats flex h-58"
>
<div
v-for=
"item in taskStats"
:key=
"item.label"
class=
"stat-item flex-1 flex-col flex-center"
>
<span
class=
"fs-12 color-888 lh-20"
>
{{
item
.
label
}}
</span>
<span
class=
"fs-12 fw-700"
:class=
"item.valueClass"
>
{{
item
.
value
}}
</span>
</div>
</div>
<div
class=
"task-tabs flex-items-center h-40 px-12"
>
<div
v-for=
"item in taskTabs"
:key=
"item.value"
class=
"task-tab h-40 lh-40 px-14 fs-14 fw-700 pointer"
:class=
"
{ active: activeTaskTab === item.value }" @click="switchTaskTab(item.value)">
{{
item
.
label
}}
</div>
</div>
<div
class=
"task-filter px-12 py-12"
>
<div
class=
"flex flex-wrap gap-8"
>
<span
v-for=
"item in currentTaskFilters"
:key=
"item.value"
class=
"filter-chip h-28 lh-28 px-12 round-6 fs-12 fw-700 pointer"
:class=
"
{ active: activeTaskFilter === item.value }" @click="activeTaskFilter = item.value">
{{
item
.
label
}}
</span>
</div>
</div>
<section
class=
"task-list flex-1 over-y-auto hide-scroll px-12 py-8"
>
<template
v-for=
"group in filteredTaskGroups"
:key=
"group.date"
>
<div
class=
"date-title fs-12 fw-700 color-888 mt-4 mb-8"
>
{{
group
.
date
}}
</div>
<div
v-for=
"item in group.items"
:key=
"item.id"
class=
"task-card bg-fff round-8 p-12 mb-12"
>
<div
class=
"flex gap-12"
>
<img
:src=
"item.cover"
:alt=
"item.title"
class=
"task-cover wh-64 round-6 cover"
/>
<div
class=
"flex-1 hidden"
>
<div
class=
"flex-between gap-8"
>
<div
class=
"fs-14 fw-700 color-000 lh-22 ellipsis"
>
{{
item
.
title
}}
</div>
<span
class=
"status-tag h-24 lh-24 px-8 round-4 fs-12 fw-700"
:class=
"item.statusClass"
>
{{
item
.
status
}}
</span>
</div>
<div
class=
"fs-10 color-888 lh-20"
>
{{
item
.
type
}}
</div>
<div
class=
"flex-between"
>
<div
class=
"fs-12 color-888"
>
消耗积分
<span
class=
"cost-value fw-700"
>
{{
item
.
cost
}}
</span>
</div>
<span
class=
"fs-10 color-888"
>
{{
item
.
time
}}
</span>
</div>
</div>
</div>
<div
v-if=
"item.progressText"
class=
"progress-box mt-12 p-10 round-6"
>
<div
class=
"flex-between fs-10 color-888 mb-8"
>
<span
class=
"flex-items-center gap-4"
>
<el-icon>
<Clock
/>
</el-icon>
{{
item
.
progressText
}}
</span>
<span>
{{
item
.
progress
}}
%
</span>
</div>
<el-progress
:percentage=
"item.progress"
:show-text=
"false"
:stroke-width=
"5"
/>
</div>
<div
class=
"task-card-footer flex justify-end gap-8 mt-12 pt-12"
>
<el-button
v-if=
"item.canCancel"
class=
"h-28 px-12 fs-12"
>
<el-icon>
<Close
/>
</el-icon>
<span>
取消任务
</span>
</el-button>
<el-button
type=
"primary"
class=
"h-28 px-12 fs-12 border-none enter-btn"
>
<el-icon>
<View
/>
</el-icon>
<span>
进入会话
</span>
</el-button>
</div>
</div>
</
template
>
</section>
<footer
class=
"drawer-footer flex-center h-40 fs-12 color-888"
>
<span
v-if=
"activeTaskTab === 'recent'"
>
近期任务仅展示最近20条,更多请查看
<span
class=
"color-main fw-700 pointer"
@
click=
"switchTaskTab('all')"
>
全部任务
</span>
</span>
<span
v-else
>
共展示 {{ filteredTaskCount }} 个任务
</span>
</footer>
</div>
</el-drawer>
</template>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
}
from
'vue'
;
import
{
Clock
,
Close
,
Refresh
,
View
}
from
'@element-plus/icons-vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
peopleImg
from
'@/static/image/meinv.png'
;
type
TaskStatus
=
'success'
|
'queue'
|
'failed'
;
type
TaskItem
=
{
id
:
string
;
title
:
string
;
type
:
string
;
category
:
string
;
date
:
string
;
time
:
string
;
cost
:
string
;
cover
:
string
;
status
:
string
;
statusClass
:
string
;
statusKey
:
TaskStatus
;
progress
?:
number
;
progressText
?:
string
;
canCancel
?:
boolean
;
};
const
props
=
defineProps
<
{
modelValue
:
boolean
;
}
>
();
const
emit
=
defineEmits
<
{
'update:modelValue'
:
[
value
:
boolean
];
}
>
();
const
drawerVisible
=
computed
({
get
:
()
=>
props
.
modelValue
,
set
:
(
value
)
=>
emit
(
'update:modelValue'
,
value
),
});
const
activeTaskTab
=
ref
(
'recent'
);
const
activeTaskFilter
=
ref
(
'agent'
);
const
taskTabs
=
[
{
label
:
'近期任务'
,
value
:
'recent'
},
{
label
:
'全部任务'
,
value
:
'all'
},
];
const
recentTaskFilters
=
[
{
label
:
'Agent创作'
,
value
:
'agent'
},
{
label
:
'工具'
,
value
:
'tool'
},
];
const
allTaskFilters
=
[
{
label
:
'全部'
,
value
:
'all'
},
{
label
:
'Agent'
,
value
:
'agent'
},
{
label
:
'快速创作'
,
value
:
'quick'
},
{
label
:
'视频去水印'
,
value
:
'watermark'
},
{
label
:
'字幕擦除'
,
value
:
'subtitle'
},
{
label
:
'画质增强'
,
value
:
'quality'
},
{
label
:
'数字人分身'
,
value
:
'digital'
},
{
label
:
'模特换衣'
,
value
:
'tryOn'
},
{
label
:
'爆款复制'
,
value
:
'copy'
},
{
label
:
'AI视频素材'
,
value
:
'videoMaterial'
},
{
label
:
'AI图片素材'
,
value
:
'imageMaterial'
},
];
const
taskStats
=
[
{
label
:
'排队中'
,
value
:
6
,
valueClass
:
'color-333'
},
{
label
:
'生成中'
,
value
:
1
,
valueClass
:
'color-main'
},
{
label
:
'生成失败'
,
value
:
2
,
valueClass
:
'failed-text'
},
];
const
taskGroups
=
[
{
date
:
'9月1日'
,
items
:
[
{
id
:
'task-1'
,
title
:
'流量'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'9月1日'
,
time
:
'08:40'
,
cost
:
'-0.00'
,
cover
:
productImg
,
status
:
'生成成功'
,
statusClass
:
'success'
,
statusKey
:
'success'
,
},
{
id
:
'task-2'
,
title
:
'防晒霜SPF50权威检测图'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'9月1日'
,
time
:
'08:16'
,
cost
:
'-15.00'
,
cover
:
productImg
,
status
:
'生成成功'
,
statusClass
:
'success'
,
statusKey
:
'success'
,
},
{
id
:
'task-3'
,
title
:
'看看卡萨斯'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'9月1日'
,
time
:
'05:39'
,
cost
:
'-5.00'
,
cover
:
productImg
,
status
:
'生成成功'
,
statusClass
:
'success'
,
statusKey
:
'success'
,
},
],
},
{
date
:
'8月31日'
,
items
:
[
{
id
:
'task-4'
,
title
:
'请结合使用已投放素材和原料库为我生成一个短视频广告'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'8月31日'
,
time
:
'07:32'
,
cost
:
'-0.00'
,
cover
:
peopleImg
,
status
:
'生成成功'
,
statusClass
:
'success'
,
statusKey
:
'success'
,
},
{
id
:
'task-5'
,
title
:
'用我的「商品」拍一条营销视频'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'8月31日'
,
time
:
'07:31'
,
cost
:
'-0.00'
,
cover
:
productImg
,
status
:
'排队中'
,
statusClass
:
'queue'
,
statusKey
:
'queue'
,
progress
:
0
,
progressText
:
'等待计算资源'
,
canCancel
:
true
,
},
{
id
:
'task-6'
,
title
:
'1'
,
type
:
'Agent创作'
,
category
:
'agent'
,
date
:
'8月31日'
,
time
:
'07:31'
,
cost
:
'-0.00'
,
cover
:
productImg
,
status
:
'生成成功'
,
statusClass
:
'success'
,
statusKey
:
'success'
,
},
],
},
]
satisfies
Array
<
{
date
:
string
;
items
:
TaskItem
[]
}
>
;
const
currentTaskFilters
=
computed
(()
=>
(
activeTaskTab
.
value
===
'recent'
?
recentTaskFilters
:
allTaskFilters
));
const
filteredTaskGroups
=
computed
(()
=>
{
const
filterValue
=
activeTaskFilter
.
value
;
return
taskGroups
.
map
((
group
)
=>
({
...
group
,
items
:
group
.
items
.
filter
((
item
)
=>
filterValue
===
'all'
||
item
.
category
===
filterValue
),
}))
.
filter
((
group
)
=>
group
.
items
.
length
);
});
const
filteredTaskCount
=
computed
(()
=>
filteredTaskGroups
.
value
.
reduce
((
total
,
group
)
=>
total
+
group
.
items
.
length
,
0
));
const
switchTaskTab
=
(
value
:
string
)
=>
{
activeTaskTab
.
value
=
value
;
activeTaskFilter
.
value
=
value
===
'recent'
?
'agent'
:
'all'
;
};
</
script
>
<
style
scoped
lang=
"scss"
>
.drawer-header
{
border-bottom
:
1px
solid
#edf2f7
;
}
.count-tag
,
.progress-box
{
background
:
#f3f6fb
;
}
.task-stats
,
.drawer-footer
{
background
:
#f8fafc
;
}
.stat-item
+
.stat-item
{
border-left
:
1px
solid
#dce6f2
;
}
.failed-text
,
.cost-value
{
color
:
#ff5a00
;
}
.task-tabs
,
.task-filter
,
.drawer-footer
{
border-top
:
1px
solid
#edf2f7
;
}
.task-tabs
{
border-bottom
:
1px
solid
#edf2f7
;
}
.task-tab
{
color
:
#536883
;
border-bottom
:
2px
solid
transparent
;
&
.active
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
border-bottom-color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
.filter-chip
{
color
:
#536883
;
background
:
#f3f6fb
;
&
.active
{
color
:
#fff
;
background
:
#17243a
;
}
}
.task-card
{
border
:
1px
solid
#dce6f2
;
box-shadow
:
0
2px
8px
rgba
(
20
,
38
,
70
,
.06
);
}
.task-cover
{
flex
:
0
0
64px
;
background
:
#f3f6fb
;
}
.status-tag.success
{
color
:
#00a86b
;
background
:
#e9fbf2
;
}
.status-tag.queue
{
color
:
#536883
;
background
:
#f3f6fb
;
}
.status-tag.failed
{
color
:
#ff5a00
;
background
:
#fff2e8
;
}
.task-card-footer
{
border-top
:
1px
solid
#edf2f7
;
}
.enter-btn
{
background
:
#7d28ff
;
}
:deep
(
.progress-box
.el-progress-bar__outer
)
{
background
:
#dce6f2
;
}
:deep
(
.progress-box
.el-progress-bar__inner
)
{
background
:
#8aa0bf
;
}
</
style
>
<
style
>
.history-drawer
{
width
:
380px
!important
;
box-shadow
:
0
0
8px
#bbb
;
}
.history-drawer
.el-drawer__body
{
padding
:
0
!important
;
}
</
style
>
apps/web-vue/src/views/agent/components/Step1.vue
0 → 100644
View file @
048729dc
<
template
>
<main
class=
"flex-1 flex-col"
>
<div
class=
"flex-1 over-y-auto hide-scroll py-20 px-20"
>
<div>
<div
class=
"mb-10"
>
1. 商品参考图
<span
class=
"fs-12 color-888"
>
(上传丰富的参考图有助于提升模型的生成质量)
</span>
</div>
<UploadFile
v-model=
"referenceFiles"
:width=
"104"
:show-file-list=
"false"
/>
</div>
<div
class=
"mt-20"
>
<div
class=
"fw-600"
>
2. 商品详细信息
</div>
<div
class=
"flex gap-12 flex-wrap mt-10"
>
<div
class=
"base-card px-16 py-10 round-10 bg-fff"
>
<div
class=
"mb-10"
>
商品名称
</div>
<el-input
v-model=
"product.name"
class=
"plain-input"
/>
</div>
<div
class=
"base-card px-16 py-10 round-10 bg-fff"
>
<div
class=
"mb-10"
>
商品行业
</div>
<el-input
v-model=
"product.industry"
class=
"plain-input"
/>
</div>
<div
class=
"base-card px-16 py-10 round-10 bg-fff"
>
<div
class=
"mb-10"
>
商品品类
</div>
<el-input
v-model=
"product.category"
class=
"plain-input"
/>
</div>
</div>
<div
class=
"flex flex-wrap gap-12 mt-20"
>
<div
v-for=
"section in productSections"
:key=
"section.key"
class=
"detail-card flex-col p-18 round-10 bg-fff"
:class=
"
{ editing: isSectionEditing(section.key) }">
<div
class=
"flex-between mb-14"
>
<div
class=
"fs-14 fw-600"
>
{{
section
.
title
}}
</div>
<el-icon
class=
"edit-icon pointer"
@
click
.
stop=
"toggleSectionEdit(section.key)"
>
<Check
v-if=
"isSectionEditing(section.key)"
/>
<Edit
v-else
/>
</el-icon>
</div>
<template
v-if=
"isSectionEditing(section.key)"
>
<template
v-if=
"section.items.length"
>
<div
v-for=
"(_, index) in section.items"
:key=
"`$
{section.key}-${index}`"
class="editable-row flex-items-center gap-8 h-34 px-8 mb-6 round-4">
<span
class=
"dot flex-center wh-6 round"
></span>
<el-input
v-model=
"section.items[index]"
class=
"row-input flex-1"
/>
<el-icon
class=
"delete-line color-999 pointer"
@
click=
"removeSectionItem(section.key, index)"
>
<Delete
/>
</el-icon>
</div>
</
template
>
<div
v-else
class=
"fs-14 color-666 mb-12"
>
暂无{{ section.title }}
</div>
<div
class=
"add-row flex-items-center gap-8 mt-8"
>
<el-input
v-model=
"section.draft"
class=
"add-input flex-1"
:placeholder=
"`添加${section.title}`"
@
keyup
.
enter=
"addSectionItem(section.key)"
/>
<el-button
type=
"primary"
class=
"add-btn wh-34 p-0 border-none"
@
click=
"addSectionItem(section.key)"
>
<el-icon>
<Plus
/>
</el-icon>
</el-button>
</div>
</template>
<
template
v-else
>
<div
v-if=
"section.items.length"
class=
"view-list flex-col gap-8"
>
<div
v-for=
"item in section.items"
:key=
"item"
class=
"view-row flex-items-center gap-8 fs-12 lh-22 color-333"
>
<span
class=
"dot flex-center wh-6 round"
></span>
<span>
{{
item
}}
</span>
</div>
</div>
<div
v-else
class=
"fs-14 color-666"
>
暂无
{{
section
.
title
}}
</div>
</
template
>
</div>
</div>
</div>
</div>
<footer
class=
"flex justify-end pt-12 pb-12 pr-20"
style=
"border-top: 1px solid #eee;"
>
<el-button
class=
"fs-12"
>
一键成片
</el-button>
<el-button
type=
"primary"
class=
"fs-12 border-none create-btn"
@
click=
"emit('createRecord')"
>
生成创意与分镜
</el-button>
</footer>
</main>
</template>
<
script
setup
lang=
"ts"
>
import
{
reactive
,
ref
}
from
'vue'
;
import
type
{
UploadUserFile
}
from
'element-plus'
;
import
{
Check
,
Delete
,
Edit
,
Plus
}
from
'@element-plus/icons-vue'
;
import
UploadFile
from
'@/components/Upload/index.vue'
;
type
ProductSection
=
{
key
:
string
;
title
:
string
;
draft
:
string
;
items
:
string
[];
};
const
emit
=
defineEmits
<
{
createRecord
:
[];
}
>
();
const
product
=
reactive
({
name
:
'看看卡萨斯'
,
industry
:
'电商零售'
,
category
:
'待补充具体品类'
,
});
const
referenceFiles
=
ref
<
UploadUserFile
[]
>
([]);
const
editingSectionKeys
=
ref
<
string
[]
>
([]);
const
productSections
=
reactive
<
ProductSection
[]
>
([
{
key
:
'selling'
,
title
:
'商品卖点'
,
draft
:
''
,
items
:
[
'核心功能突出'
,
'使用简单'
,
'效果直观'
,
'适配日常场景'
],
},
{
key
:
'pain'
,
title
:
'商品痛点'
,
draft
:
''
,
items
:
[
'现有产品使用体验不佳'
,
'难以直观看到使用效果'
,
'同类商品选择成本高'
],
},
{
key
:
'target'
,
title
:
'目标人群'
,
draft
:
''
,
items
:
[
'有看看卡萨斯相关使用需求的消费者'
,
'关注实际效果的人群'
,
'重视性价比的用户'
],
},
{
key
:
'scene'
,
title
:
'适用人群和场景'
,
draft
:
''
,
items
:
[
'日常家庭使用'
,
'需要快速解决问题时使用'
,
'购买同类商品前对比选择'
],
},
{
key
:
'spec'
,
title
:
'商品规格'
,
draft
:
''
,
items
:
[
'核心功能配置'
,
'便携易用设计'
,
'适配常见使用环境'
],
},
{
key
:
'discount'
,
title
:
'优惠信息'
,
draft
:
''
,
items
:
[],
},
]);
const
findSection
=
(
key
:
string
)
=>
productSections
.
find
((
section
)
=>
section
.
key
===
key
);
const
isSectionEditing
=
(
key
:
string
)
=>
editingSectionKeys
.
value
.
includes
(
key
);
const
toggleSectionEdit
=
(
key
:
string
)
=>
{
if
(
isSectionEditing
(
key
))
{
editingSectionKeys
.
value
=
editingSectionKeys
.
value
.
filter
((
item
)
=>
item
!==
key
);
return
;
}
editingSectionKeys
.
value
=
[...
editingSectionKeys
.
value
,
key
];
};
const
addSectionItem
=
(
key
:
string
)
=>
{
const
section
=
findSection
(
key
);
const
value
=
section
?.
draft
.
trim
();
if
(
!
section
||
!
value
)
{
return
;
}
section
.
items
.
push
(
value
);
section
.
draft
=
''
;
};
const
removeSectionItem
=
(
key
:
string
,
index
:
number
)
=>
{
const
section
=
findSection
(
key
);
section
?.
items
.
splice
(
index
,
1
);
};
</
script
>
<
style
scoped
lang=
"scss"
>
.base-card
,
.detail-card
{
border
:
1px
solid
#dce6f2
;
}
.base-card
{
flex
:
0
0
calc
((
100%
-
12px
)
/
2
);
&
:hover
{
border-color
:
rgba
(
var
(
--
main-color
)
,
.6
);
}
}
.detail-card
{
flex
:
0
0
calc
((
100%
-
12px
)
/
2
);
min-height
:
158px
;
transition
:
border-color
.2s
,
box-shadow
.2s
;
&
:hover
{
border-color
:
rgba
(
var
(
--
main-color
)
,
.6
);
}
&
.editing
{
border-color
:
#dce6f2
;
}
}
.editable-row
{
background
:
#f8fafc
;
&
:hover
{
background
:
#f3f6fa
;
}
}
.dot
{
flex
:
0
0
6px
;
background
:
#c8d4e3
;
}
.edit-icon
{
color
:
#8aa0bf
;
&
:hover
{
color
:
#7d28ff
;
}
}
.delete-line
{
flex
:
0
0
18px
;
}
.add-row
{
border-top
:
1px
solid
#edf2f7
;
padding-top
:
10px
;
}
.add-btn
,
.create-btn
{
background
:
#7d28ff
;
}
:deep
(
.plain-input
.el-input__wrapper
),
:deep
(
.row-input
.el-input__wrapper
),
:deep
(
.add-input
.el-input__wrapper
)
{
padding
:
0
;
box-shadow
:
none
;
background
:
transparent
;
}
:deep
(
.plain-input
.el-input__inner
)
{
height
:
26px
;
font-size
:
14px
;
color
:
#0a1b34
;
}
:deep
(
.row-input
.el-input__inner
),
:deep
(
.add-input
.el-input__inner
)
{
height
:
30px
;
font-size
:
12px
;
}
:deep
(
.add-input
.el-input__wrapper
)
{
padding
:
0
10px
;
border
:
1px
solid
#dce6f2
;
border-radius
:
4px
;
background
:
#fff
;
}
</
style
>
apps/web-vue/src/views/agent/components/Step2.vue
0 → 100644
View file @
048729dc
<
template
>
<main
class=
"step2-main flex-1 flex hidden"
>
<aside
class=
"creative-list flex-col gap-12 py-20 pr-16 pl-16"
>
<div
v-for=
"item in creatives"
:key=
"item.id"
class=
"creative-card p-12 round-6 pointer"
:class=
"
{ active: selectedCreativeId === item.id }" @click="selectedCreativeId = item.id">
<div
class=
"flex-between mb-10"
>
<span
class=
"fs-14 fw-700 color-000"
>
{{
item
.
title
}}
</span>
<span
class=
"tag h-24 lh-24 px-8 round-4 fs-12"
>
{{
item
.
tag
}}
</span>
</div>
<div
class=
"fs-14 fw-700 color-000 mb-8 ellipsis"
>
{{
item
.
name
}}
</div>
<div
class=
"fs-12 color-666 lh-20 ellipsis-2"
>
{{
item
.
desc
}}
</div>
</div>
</aside>
<div
class=
"flex-1 flex-col"
>
<section
class=
"step2-content flex-1 over-y-auto hide-scroll py-20 pr-20"
>
<div
class=
"creative-detail bg-fff round-8 hidden"
>
<div
class=
"p-24"
>
<div
class=
"flex-between mb-14"
>
<div
class=
"flex-items-center gap-8"
>
<span
class=
"color-main fs-12 fw-700"
>
{{
selectedCreative
.
title
}}
</span>
<span
class=
"tag h-22 lh-22 px-8 round-4 fs-12"
>
{{
selectedCreative
.
tag
}}
</span>
</div>
<el-icon
class=
"color-888 pointer"
>
<MoreFilled
/>
</el-icon>
</div>
<div
class=
" fw-700 color-000 mb-22"
>
{{
selectedCreative
.
name
}}
</div>
<div
class=
"summary-box p-16 round-8"
>
<div
class=
"fs-14 fw-700 color-000 mb-10"
>
创意概述
</div>
<div
class=
"fs-14 color-333 lh-24"
>
{{
selectedCreative
.
desc
}}
</div>
</div>
<div
class=
"flex-items-center gap-6 mt-18 pointer color-333 fs-12 fw-700"
@
click=
"isCreativeExpanded = !isCreativeExpanded"
>
<span>
{{
isCreativeExpanded
?
'收起完整创意'
:
'查看完整创意'
}}
</span>
<el-icon>
<ArrowUp
v-if=
"isCreativeExpanded"
/>
<ArrowRight
v-else
/>
</el-icon>
</div>
<div
v-if=
"isCreativeExpanded"
class=
"full-creative mt-24 pt-24"
>
<div
v-for=
"item in fullCreativeItems"
:key=
"item.title"
class=
"mb-24"
>
<div
class=
"fs-14 fw-700 color-000 mb-12"
>
{{
item
.
title
}}
</div>
<div
class=
"fs-14 color-333 lh-24"
>
{{
item
.
content
}}
</div>
</div>
</div>
</div>
<div
class=
"script-panel p-24"
>
<h3
class=
"fs-14 fw-700 color-main mb-28"
>
分镜脚本
</h3>
<section
class=
"mb-28"
>
<h4
class=
"fs-14 fw-700 color-000 mb-14"
>
1. 主体设定
</h4>
<div
class=
"subject-list flex flex-wrap gap-12"
>
<div
v-for=
"item in subjects"
:key=
"item.id"
class=
"subject-card w-220 p-12 round-8 pointer relative"
@
click=
"openSubjectDialog(item.id)"
>
<div
class=
"flex-between mb-8 "
>
<div>
<div
class=
"fs-14 fw-700 color-000 mb-6"
>
{{
item
.
title
}}
</div>
<div
class=
"fs-12 color-666 ellipsis"
>
{{
item
.
desc
}}
</div>
</div>
<el-button
class=
"wh-28 p-0 border-none bg-fff absolute top-10 right-10"
@
click
.
stop=
"openSubjectDialog(item.id)"
>
<el-icon>
<component
:is=
"item.actionIcon"
/>
</el-icon>
</el-button>
</div>
<div
class=
"subject-cover flex-center round-6 hidden"
>
<img
v-if=
"item.image"
:src=
"item.image"
:alt=
"item.title"
class=
"w-full h-full cover"
/>
<el-icon
v-else
class=
"audio-icon"
>
<Mute
/>
</el-icon>
</div>
</div>
</div>
</section>
<section>
<h4
class=
"fs-14 fw-700 color-000 mb-18"
>
2. 分镜描述
</h4>
<div
v-for=
"shot in shots"
:key=
"shot.id"
class=
"shot-block mb-28"
>
<div
class=
"shot-title flex-items-center gap-8 mb-10 fs-12 fw-700 color-333"
>
<span
class=
"shot-dot wh-6 round"
></span>
<span>
{{
shot
.
title
}}
</span>
</div>
<div
class=
"shot-table round-8 hidden"
>
<div
class=
"table-head flex fs-12 fw-700 color-000"
>
<div
class=
"dialogue-cell p-14"
>
人物台词
</div>
<div
class=
"picture-cell p-14"
>
画面描述
</div>
</div>
<div
v-for=
"row in shot.rows"
:key=
"row.id"
class=
"table-row flex"
>
<div
class=
"dialogue-cell p-14 fs-12 color-333 lh-24"
>
<span
class=
"role-tag h-24 lh-24 px-8 round-4 fw-700 color-main mr-6"
>
{{
row
.
speaker
}}
</span>
<span>
{{
row
.
dialogue
}}
</span>
</div>
<div
class=
"picture-cell p-14 fs-12 color-333 lh-24"
>
<span
class=
"role-tag h-24 lh-24 px-8 round-4 fw-700 color-main mr-6"
>
{{
row
.
actor
}}
</span>
<span>
{{
row
.
picture
}}
</span>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</section>
<footer
class=
"flex justify-end pt-12 pb-12 pr-20"
style=
"border-top: 1px solid #eee;"
>
<el-button
type=
"primary"
class=
"fs-12 border-none create-btn"
@
click=
"emit('createPreview')"
>
生成视频预览
</el-button>
</footer>
</div>
</main>
<Step2SubjectDialog
v-model=
"subjectDialogVisible"
:subject-type=
"activeSubjectType"
/>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
,
type
Component
}
from
'vue'
;
import
{
ArrowRight
,
ArrowUp
,
MoreFilled
,
Mute
,
Plus
,
VideoCamera
}
from
'@element-plus/icons-vue'
;
import
Step2SubjectDialog
from
'./Step2SubjectDialog.vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
peopleImg
from
'@/static/image/meinv.png'
;
type
CreativeItem
=
{
id
:
string
;
title
:
string
;
tag
:
string
;
name
:
string
;
desc
:
string
;
};
type
SubjectType
=
'product'
|
'actor'
|
'voice'
;
type
SubjectItem
=
{
id
:
SubjectType
;
title
:
string
;
desc
:
string
;
image
?:
string
;
actionIcon
:
Component
;
};
type
ShotRow
=
{
id
:
string
;
speaker
:
string
;
actor
:
string
;
dialogue
:
string
;
picture
:
string
;
};
type
ShotItem
=
{
id
:
string
;
title
:
string
;
rows
:
ShotRow
[];
};
const
emit
=
defineEmits
<
{
createPreview
:
[];
}
>
();
const
selectedCreativeId
=
ref
(
'creative-1'
);
const
isCreativeExpanded
=
ref
(
false
);
const
subjectDialogVisible
=
ref
(
false
);
const
activeSubjectType
=
ref
<
SubjectType
>
(
'product'
);
const
creatives
:
CreativeItem
[]
=
[
{
id
:
'creative-1'
,
title
:
'创意 1'
,
tag
:
'痛点实测'
,
name
:
'雨天视线危机'
,
desc
:
'从雨天挡风玻璃油膜造成视线模糊的真实痛点切入,通过清洁前后的直观对比突出产品效果。'
,
},
{
id
:
'creative-2'
,
title
:
'创意 2'
,
tag
:
'场景转化'
,
name
:
'夜间炫光对比'
,
desc
:
'聚焦夜间会车时油膜放大灯光炫光的问题,通过左右对比强化购买理由。'
,
},
{
id
:
'creative-3'
,
title
:
'创意 3'
,
tag
:
'效率展示'
,
name
:
'30秒快速去膜'
,
desc
:
'用30秒计时挑战串联完整使用过程,以快节奏实操展示简单高效的清洁效果。'
,
},
];
const
selectedCreative
=
computed
(()
=>
creatives
.
find
((
item
)
=>
item
.
id
===
selectedCreativeId
.
value
)
||
creatives
[
0
]);
const
fullCreativeItems
=
[
{
title
:
'一、推荐理由'
,
content
:
'以“雨天视线模糊”这一高频驾驶安全隐患为切入点,先建立车主焦虑,再用清洁前后对比直接证明产品效果,购买理由清晰。'
,
},
{
title
:
'二、核心卖点'
,
content
:
'强力去油膜、擦拭无残留、视野通透、自带海绵擦头'
,
},
{
title
:
'三、卖点表现形式'
,
content
:
'雨天实景痛点 + 半边玻璃清洁对比 + 清洁后驾驶视角展示'
,
},
];
const
subjects
:
SubjectItem
[]
=
[
{
id
:
'product'
,
title
:
'商品'
,
desc
:
'防晒霜SPF50权威检测图'
,
image
:
productImg
,
actionIcon
:
Plus
,
},
{
id
:
'actor'
,
title
:
'人物形象 1'
,
desc
:
'一位注重生活品质和驾驶安全的车主'
,
image
:
peopleImg
,
actionIcon
:
VideoCamera
,
},
{
id
:
'voice'
,
title
:
'旁白'
,
desc
:
'广告旁白音色,专业清晰,节奏沉稳'
,
actionIcon
:
VideoCamera
,
},
];
const
openSubjectDialog
=
(
type
:
SubjectType
)
=>
{
activeSubjectType
.
value
=
type
;
subjectDialogVisible
.
value
=
true
;
};
const
shots
:
ShotItem
[]
=
[
{
id
:
'shot-1'
,
title
:
'镜头 1:痛点前置,引发用户共鸣'
,
rows
:
[
{
id
:
'shot-1-1'
,
speaker
:
'旁白'
,
actor
:
'人物形象 1'
,
dialogue
:
'一下雨,挡风玻璃就像蒙了一层雾,雨刮越刮反而越模糊。'
,
picture
:
'雨天车内第一视角,雨刮扫过后玻璃仍有大片油膜与光晕,驾驶员皱眉观察前方。'
,
},
{
id
:
'shot-1-2'
,
speaker
:
'人物形象 1'
,
actor
:
'商品'
,
dialogue
:
'别急,用这款玻璃油膜清洁擦,自己就能快速处理。'
,
picture
:
'特写展示产品外观和实际使用过程,品牌与核心卖点清晰可见。'
,
},
],
},
{
id
:
'shot-2'
,
title
:
'镜头 2:产品实测,展示去膜效果'
,
rows
:
[
{
id
:
'shot-2-1'
,
speaker
:
'旁白'
,
actor
:
'人物形象 1'
,
dialogue
:
'用它在玻璃上轻轻擦几遍,冲水后油膜很快就被带走了。'
,
picture
:
'近景展示玻璃油膜清洁擦,完成涂抹、擦拭和冲水;画面保留一半未清洁区域形成直观对比。'
,
},
{
id
:
'shot-2-2'
,
speaker
:
'人物形象 1'
,
actor
:
'商品'
,
dialogue
:
'操作简单,擦完以后玻璃清楚多了。'
,
picture
:
'特写展示产品外观和实际使用过程,品牌与核心卖点清晰可见。'
,
},
],
},
{
id
:
'shot-3'
,
title
:
'镜头 3:效果收束,强化安全价值'
,
rows
:
[
{
id
:
'shot-3-1'
,
speaker
:
'旁白'
,
actor
:
'人物形象 1'
,
dialogue
:
'玻璃重新透亮,雨天和夜间开车都安心多了。'
,
picture
:
'切回驾驶视角,挡风玻璃清晰通透,路面与车灯轮廓清楚,产品定格收尾。'
,
},
{
id
:
'shot-3-2'
,
speaker
:
'人物形象 1'
,
actor
:
'商品'
,
dialogue
:
'操作简单,擦完以后玻璃清楚多了。'
,
picture
:
'特写展示产品外观和实际使用过程,品牌与核心卖点清晰可见。'
,
},
],
},
];
</
script
>
<
style
scoped
lang=
"scss"
>
.step2-main
{
min-width
:
0
;
}
.creative-list
{
flex
:
0
0
200px
;
border-right
:
1px
solid
#dce6f2
;
}
.creative-card
{
border
:
1px
solid
#dce6f2
;
background
:
#fff
;
transition
:
border-color
.2s
,
box-shadow
.2s
;
&
:hover
,
&
.active
{
border-color
:
rgba
(
var
(
--
main-color
)
,
1
);
box-shadow
:
0
0
0
1px
rgba
(
var
(
--
main-color
)
,
.35
);
}
}
.tag
,
.role-tag
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
background
:
rgba
(
var
(
--
main-color
)
,
.08
);
}
.step2-content
{
min-width
:
0
;
padding-left
:
24px
;
}
.creative-detail
{
border
:
1px
solid
#dce6f2
;
}
.summary-box
{
background
:
#f8fafc
;
}
.full-creative
{
border-top
:
1px
solid
#edf2f7
;
}
.script-panel
{
border-top
:
1px
solid
#dce6f2
;
}
.subject-card
{
border
:
1px
solid
#dce6f2
;
background
:
#fbfdff
;
}
.subject-cover
{
height
:
180px
;
border
:
1px
dashed
#dce6f2
;
background
:
#fff
;
}
.audio-icon
{
font-size
:
42px
;
color
:
#c4cfdd
;
}
.shot-dot
{
flex
:
0
0
6px
;
background
:
rgba
(
var
(
--
main-color
)
,
1
);
}
.shot-table
{
border
:
1px
solid
#dce6f2
;
}
.table-head
{
background
:
#f8fafc
;
}
.table-row
{
border-top
:
1px
solid
#edf2f7
;
}
.dialogue-cell
,
.picture-cell
{
flex
:
1
;
min-width
:
0
;
}
.dialogue-cell
{
border-right
:
1px
solid
#dce6f2
;
}
</
style
>
apps/web-vue/src/views/agent/components/Step2SubjectDialog.vue
0 → 100644
View file @
048729dc
<
template
>
<Dialog
v-model=
"dialogVisible"
bg-color=
"#fff"
width=
"900"
>
<div
class=
"subject-dialog flex-col bg-fff round-8 hidden"
>
<header
class=
"dialog-header flex-items-center gap-16 px-26 py-10"
>
<el-icon
class=
"fs-16 color-666 pointer"
@
click=
"dialogVisible = false"
>
<Back
/>
</el-icon>
<div>
<div
class=
"fs-16 fw-700 color-000 lh-24"
>
{{
currentTitle
}}
</div>
<div
class=
"fs-12 color-888 lh-20"
>
仅修改当前创意
</div>
</div>
</header>
<section
class=
"dialog-body px-26 py-20"
>
<template
v-if=
"subjectType === 'product'"
>
<div
class=
"flex-between mb-16"
>
<div>
<div
class=
"fs-14 fw-700 color-000 lh-22"
>
商品参考图
<span
class=
"color-888 fw-400"
>
{{
productImages
.
length
}}
/6
</span>
</div>
<div
class=
"fs-12 color-888 lh-20"
>
至少保留一张商品参考图
</div>
</div>
<el-button
type=
"primary"
class=
"h-32 px-16 fs-12 border-none"
@
click=
"imageDialogVisible = true"
>
<el-icon>
<Plus
/>
</el-icon>
<span>
添加参考图
</span>
</el-button>
</div>
<div
class=
"flex gap-10"
>
<div
v-for=
"item in productImages"
:key=
"item.id"
class=
"reference-card relative w-178 h-178 round-6 hidden"
>
<img
:src=
"item.image"
:alt=
"item.name"
class=
"w-full h-full cover"
/>
<div
class=
"image-actions absolute top-8 right-8 flex-items-center gap-6"
>
<span
class=
"action-btn flex-center wh-22 round-4 pointer"
>
<el-icon
class=
"fs-12"
>
<Delete
/>
</el-icon>
</span>
</div>
</div>
</div>
</
template
>
<
template
v-else-if=
"subjectType === 'actor'"
>
<div
class=
"flex gap-20"
>
<div
class=
"actor-cover w-240 h-318 round-8 hidden"
>
<img
:src=
"actorInfo.image"
:alt=
"actorInfo.title"
class=
"w-full h-full cover"
/>
</div>
<div
class=
"flex-1 flex-col"
>
<div
class=
"info-box p-16 round-6 mb-20"
>
<div
class=
"fs-12 fw-700 color-000 mb-10"
>
形象描述
</div>
<div
class=
"fs-12 color-333 lh-22"
>
{{
actorInfo
.
desc
}}
</div>
</div>
<div
class=
"flex-between mb-12"
>
<div
class=
"fs-14 fw-700 color-000"
>
音色
</div>
<span
class=
"fs-12 fw-700 color-main pointer"
@
click=
"toggleToneCreator"
>
{{
isToneCreatorVisible
?
'取消'
:
'新增音色'
}}
</span>
</div>
<div
class=
"tone-list flex gap-8 mb-12"
>
<div
v-for=
"item in actorTones"
:key=
"item.id"
class=
"tone-item flex-center h-50 px-34 round-6 fs-12 fw-700 pointer"
:class=
"
{ active: selectedToneId === item.id }" @click="selectedToneId = item.id">
{{
item
.
name
}}
</div>
</div>
<div
class=
"info-box p-16 round-6"
>
<div
class=
"fs-12 fw-700 color-000 mb-10"
>
音色描述
</div>
<div
class=
"fs-12 color-333 lh-22"
>
{{
currentTone
?.
desc
}}
</div>
</div>
<div
v-if=
"isToneCreatorVisible"
class=
"tone-creator p-12 round-6 mt-12"
>
<el-input
v-model=
"toneCreatorText"
class=
"tone-textarea"
type=
"textarea"
resize=
"none"
:rows=
"4"
placeholder=
"请输入音色描述"
/>
<div
class=
"flex justify-end mt-14"
>
<el-button
type=
"primary"
class=
"h-32 px-16 fs-12 border-none"
:disabled=
"isGenerateToneDisabled"
@
click=
"generateTone"
>
生成音色
</el-button>
</div>
</div>
</div>
</div>
</
template
>
<
template
v-else
>
<div
class=
"info-box p-16 round-6 mb-20"
>
<div
class=
"fs-12 fw-700 color-000 mb-10"
>
旁白描述
</div>
<div
class=
"fs-12 color-333 lh-22"
>
{{
voiceInfo
.
desc
}}
</div>
</div>
<div
class=
"flex-between mb-12"
>
<div
class=
"fs-14 fw-700 color-000"
>
音色
</div>
<span
class=
"fs-12 fw-700 color-main pointer"
@
click=
"toggleToneCreator"
>
{{
isToneCreatorVisible
?
'取消'
:
'新增音色'
}}
</span>
</div>
<div
class=
"tone-list flex gap-8 mb-12"
>
<div
v-for=
"item in voiceTones"
:key=
"item.id"
class=
"tone-item flex-center h-50 px-34 round-6 fs-12 fw-700 pointer"
:class=
"
{ active: selectedToneId === item.id }" @click="selectedToneId = item.id">
{{
item
.
name
}}
</div>
</div>
<div
class=
"info-box p-16 round-6"
>
<div
class=
"fs-12 fw-700 color-000 mb-10"
>
音色描述
</div>
<div
class=
"fs-12 color-333 lh-22"
>
{{
currentTone
?.
desc
}}
</div>
</div>
<div
v-if=
"isToneCreatorVisible"
class=
"tone-creator p-12 round-6 mt-12"
>
<el-input
v-model=
"toneCreatorText"
class=
"tone-textarea"
type=
"textarea"
resize=
"none"
:rows=
"4"
placeholder=
"请输入音色描述"
/>
<div
class=
"flex justify-end mt-14"
>
<el-button
type=
"primary"
class=
"h-32 px-16 fs-12 border-none"
:disabled=
"isGenerateToneDisabled"
@
click=
"generateTone"
>
生成音色
</el-button>
</div>
</div>
</
template
>
</section>
<footer
class=
"dialog-footer flex justify-end px-26 py-16"
>
<el-button
class=
"px-18 fs-12"
@
click=
"dialogVisible = false"
>
取消
</el-button>
<el-button
type=
"primary"
class=
"px-20 fs-12 border-none"
@
click=
"confirmSubject"
>
应用
</el-button>
</footer>
</div>
</Dialog>
<AgentProductImageDialog
v-model=
"imageDialogVisible"
data-type=
"goods"
@
confirm=
"handleReferenceImagesConfirm"
/>
</template>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
,
watch
}
from
'vue'
;
import
{
Back
,
Delete
,
Plus
}
from
'@element-plus/icons-vue'
;
import
AgentProductImageDialog
from
'./AgentProductImageDialog.vue'
;
import
Dialog
from
'@/components/Dialog/index.vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
peopleImg
from
'@/static/image/meinv.png'
;
type
SubjectType
=
'product'
|
'actor'
|
'voice'
;
type
ToneItem
=
{
id
:
string
;
name
:
string
;
desc
:
string
;
};
const
props
=
defineProps
<
{
modelValue
:
boolean
;
subjectType
:
SubjectType
;
}
>
();
const
emit
=
defineEmits
<
{
'update:modelValue'
:
[
value
:
boolean
];
confirm
:
[
value
:
{
subjectType
:
SubjectType
;
toneId
:
string
}];
}
>
();
const
selectedToneId
=
ref
(
''
);
const
imageDialogVisible
=
ref
(
false
);
const
isToneCreatorVisible
=
ref
(
false
);
const
toneCreatorText
=
ref
(
''
);
const
productImages
=
ref
([
{
id
:
'product-1'
,
name
:
'商品参考图 1'
,
image
:
productImg
,
},
{
id
:
'product-2'
,
name
:
'商品参考图 2'
,
image
:
peopleImg
,
},
]);
const
actorInfo
=
{
title
:
'人物形象 1'
,
image
:
peopleImg
,
desc
:
'一位注重生活品质和驾驶安全的年轻车主,表达自然可信。'
,
};
const
voiceInfo
=
{
desc
:
'广告旁白音色,专业清晰,节奏稳健,重点信息有强调。'
,
};
const
actorTones
=
ref
<
ToneItem
[]
>
([
{
id
:
'actor-tone-1'
,
name
:
'音色 1'
,
desc
:
'自然生活化口吻,语速适中,适合真实体验类内容。'
,
},
{
id
:
'actor-tone-2'
,
name
:
'音色 2'
,
desc
:
'达到'
,
},
]);
const
voiceTones
=
ref
<
ToneItem
[]
>
([
{
id
:
'voice-tone-1'
,
name
:
'音色 1'
,
desc
:
'青年男性,广告旁白风格,专业讲解口吻,音色清晰稳重'
,
},
]);
const
titleMap
:
Record
<
SubjectType
,
string
>
=
{
product
:
'防晒霜SPF50权威检测图'
,
actor
:
'人物形象 1设定'
,
voice
:
'旁白设定'
,
};
const
dialogVisible
=
computed
({
get
:
()
=>
props
.
modelValue
,
set
:
(
value
)
=>
emit
(
'update:modelValue'
,
value
),
});
const
currentTitle
=
computed
(()
=>
titleMap
[
props
.
subjectType
]);
const
currentToneList
=
computed
(()
=>
(
props
.
subjectType
===
'voice'
?
voiceTones
.
value
:
actorTones
.
value
));
const
currentTone
=
computed
(()
=>
currentToneList
.
value
.
find
((
item
)
=>
item
.
id
===
selectedToneId
.
value
));
const
isGenerateToneDisabled
=
computed
(()
=>
toneCreatorText
.
value
.
trim
()
===
''
);
watch
(
()
=>
props
.
subjectType
,
(
value
)
=>
{
selectedToneId
.
value
=
value
===
'voice'
?
'voice-tone-1'
:
'actor-tone-2'
;
isToneCreatorVisible
.
value
=
false
;
toneCreatorText
.
value
=
''
;
},
{
immediate
:
true
},
);
const
confirmSubject
=
()
=>
{
emit
(
'confirm'
,
{
subjectType
:
props
.
subjectType
,
toneId
:
selectedToneId
.
value
,
});
dialogVisible
.
value
=
false
;
};
const
handleReferenceImagesConfirm
=
(
ids
:
string
[])
=>
{
const
selectedImages
=
ids
.
filter
((
id
)
=>
!
productImages
.
value
.
some
((
item
)
=>
item
.
id
===
id
))
.
map
((
id
,
index
)
=>
({
id
,
name
:
`商品参考图
${
productImages
.
value
.
length
+
index
+
1
}
`
,
image
:
index
%
2
===
0
?
productImg
:
peopleImg
,
}));
productImages
.
value
=
[...
productImages
.
value
,
...
selectedImages
].
slice
(
0
,
6
);
};
const
toggleToneCreator
=
()
=>
{
isToneCreatorVisible
.
value
=
!
isToneCreatorVisible
.
value
;
if
(
!
isToneCreatorVisible
.
value
)
{
toneCreatorText
.
value
=
''
;
}
};
const
generateTone
=
()
=>
{
if
(
isGenerateToneDisabled
.
value
)
{
return
;
}
const
list
=
props
.
subjectType
===
'voice'
?
voiceTones
.
value
:
actorTones
.
value
;
const
tone
=
{
id
:
`
${
props
.
subjectType
}
-tone-
${
Date
.
now
()}
`
,
name
:
`音色
${
list
.
length
+
1
}
`
,
desc
:
toneCreatorText
.
value
.
trim
(),
};
list
.
push
(
tone
);
selectedToneId
.
value
=
tone
.
id
;
isToneCreatorVisible
.
value
=
false
;
toneCreatorText
.
value
=
''
;
};
</
script
>
<
style
scoped
lang=
"scss"
>
.subject-dialog
{
border
:
1px
solid
#dce6f2
;
}
.dialog-header
{
border-bottom
:
1px
solid
#dce6f2
;
}
.dialog-body
{
min-height
:
272px
;
}
.dialog-footer
{
border-top
:
1px
solid
#dce6f2
;
}
.reference-card
,
.actor-cover
{
background
:
#f3f6fb
;
}
.image-actions
{
opacity
:
0
;
transition
:
opacity
.2s
;
}
.reference-card
:hover
.image-actions
{
opacity
:
1
;
}
.action-btn
{
color
:
#fff
;
background
:
rgba
(
0
,
0
,
0
,
.42
);
}
.info-box
{
border
:
1px
solid
#dce6f2
;
background
:
#f8fafc
;
}
.tone-creator
{
border
:
1px
solid
#dce6f2
;
background
:
#fbfdff
;
}
.tone-item
{
min-width
:
108px
;
border
:
1px
solid
#dce6f2
;
color
:
#333
;
background
:
#fff
;
&
.active
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
border-color
:
rgba
(
var
(
--
main-color
)
,
1
);
background
:
rgba
(
var
(
--
main-color
)
,
.06
);
}
}
:deep
(
.tone-textarea
.el-textarea__inner
)
{
min-height
:
96px
!
important
;
border-radius
:
6px
;
font-size
:
12px
;
line-height
:
22px
;
box-shadow
:
0
0
0
1px
#dce6f2
inset
;
}
</
style
>
apps/web-vue/src/views/agent/components/Step3.vue
0 → 100644
View file @
048729dc
<
template
>
<main
class=
"step3-main flex-1 flex-col hidden"
>
<div
class=
"preview-workspace flex-1 flex-col hidden"
>
<section
class=
"flex-1 flex hidden"
>
<aside
class=
"plan-tabs flex-col items-center gap-22 py-28"
>
<div
v-for=
"item in plans"
:key=
"item.id"
class=
"plan-tab flex-col flex-center gap-8 py-10 round-6 pointer relative"
:class=
"
{ active: selectedPlanId === item.id }" @click="selectPlan(item.id)">
<el-icon
class=
"fs-18"
>
<VideoCamera
/>
</el-icon>
<span
class=
"fs-12 fw-700"
>
{{
item
.
name
}}
</span>
<span
v-if=
"item.dot"
class=
"notice-dot absolute wh-8 round"
></span>
</div>
</aside>
<aside
class=
"video-panel flex-col items-center px-16 py-18"
>
<div
class=
"preview-cover relative w-188 h-334 round-8 hidden"
>
<img
v-if=
"!previewCoverErrorMap[selectedPlan.id]"
:src=
"selectedPlan.cover"
:alt=
"selectedPlan.name"
class=
"w-full h-full cover"
@
error=
"handlePreviewCoverError"
/>
<div
v-else
class=
"preview-placeholder flex-col flex-center w-full h-full"
>
<el-icon
class=
"fs-30 color-888 mb-8"
>
<VideoCamera
/>
</el-icon>
<span
class=
"fs-12 color-888"
>
视频加载中
</span>
</div>
<span
class=
"recommend-tag absolute top-8 left-8 h-24 lh-24 px-10 round-4 fs-12 fw-700 color-fff"
>
{{
selectedPlan
.
name
}}
</span>
<span
class=
"play-btn absolute flex-center wh-50 round pointer"
>
<el-icon
class=
"fs-30 color-main"
>
<VideoPlay
/>
</el-icon>
</span>
</div>
<div
class=
"flex-items-center gap-8 mt-16"
>
<el-button
class=
"wh-32 p-0"
>
<el-icon>
<VideoPlay
/>
</el-icon>
</el-button>
<el-button
class=
"wh-32 p-0"
>
<el-icon>
<Microphone
/>
</el-icon>
</el-button>
<span
class=
"fs-12 color-000"
>
00:00
</span>
<span
class=
"fs-12 color-888"
>
/ 00:21
</span>
</div>
</aside>
<section
class=
"material-panel flex-1 px-16 py-18"
>
<h3
class=
"fs-16 fw-700 color-000 mb-14"
>
适合“画面1”的原料
</h3>
<div
class=
"material-list flex gap-8 mb-16"
>
<div
class=
"add-material flex-col flex-center w-66 h-66 round-6 pointer"
>
<el-icon
class=
"fs-18 color-666 mb-4"
>
<Plus
/>
</el-icon>
<span
class=
"fs-12 color-888"
>
新增视频
</span>
</div>
<div
v-for=
"item in selectedPlan.materials"
:key=
"item.id"
class=
"material-card relative w-66 h-66 round-6 hidden pointer"
:class=
"
{ active: selectedMaterialId === item.id }" @click="selectedMaterialId = item.id">
<img
:src=
"item.image"
:alt=
"item.name"
class=
"w-full h-full cover"
/>
<span
class=
"ai-tag absolute top-0 left-0 h-20 lh-20 px-6 fs-10 fw-700 color-fff"
>
AI
</span>
</div>
</div>
<div
class=
"description-box p-16 round-8"
>
<div
class=
"flex-between mb-8"
>
<div>
<div
class=
"fs-14 fw-700 color-000 mb-4"
>
画面描述
</div>
<div
class=
"fs-12 color-888"
>
原料时长:7秒 · 所用片段:0.0-7.0秒
</div>
</div>
<div
class=
"flex-items-center gap-22 color-666 fs-12"
>
<span
class=
"flex-items-center gap-4 pointer"
>
<el-icon>
<Crop
/>
</el-icon>
裁剪
</span>
<span
class=
"flex-items-center gap-4 pointer"
>
<el-icon>
<MagicStick
/>
</el-icon>
优化
</span>
</div>
</div>
<div
class=
"section-line mb-14"
></div>
<div
class=
"script-scroll over-y-auto pr-8"
>
<div
v-for=
"item in shotDescriptions"
:key=
"item.time"
class=
"mb-14"
>
<div
class=
"fs-12 fw-700 color-333 lh-22"
>
{{
item
.
time
}}
{{
item
.
scene
}}
</div>
<div
class=
"fs-12 color-333 lh-22 mt-6"
>
口播内容:
{{
item
.
speech
}}
</div>
</div>
</div>
<div
class=
"section-line mt-12 mb-12"
></div>
<div
class=
"flex-items-center gap-8"
>
<span
class=
"fs-12 fw-700 color-333"
>
参考主体:
</span>
<span
class=
"subject-icon flex-center wh-32 round-4"
>
<el-icon>
<Box
/>
</el-icon>
</span>
<span
class=
"subject-icon flex-center wh-32 round-4 hidden"
>
<img
:src=
"peopleImg"
alt=
"人物形象"
class=
"w-full h-full cover"
/>
</span>
<span
class=
"subject-icon flex-center wh-32 round-4"
>
<el-icon>
<Microphone
/>
</el-icon>
</span>
</div>
</div>
</section>
</section>
<section
class=
"timeline-panel flex-between px-10 py-10"
>
<div
class=
"timeline-inner flex-col"
>
<div
class=
"flex-items-center gap-20 color-666 fs-16 mb-16"
>
<el-icon
class=
"pointer"
>
<Refresh
/>
</el-icon>
<el-icon
class=
"pointer"
>
<MagicStick
/>
</el-icon>
<el-icon
class=
"pointer"
>
<Plus
/>
</el-icon>
</div>
<div
class=
"flex gap-12"
>
<div
v-for=
"item in timelineItems"
:key=
"item.id"
class=
"timeline-card relative w-112 h-62 round-4 hidden pointer"
:class=
"
{ active: selectedTimelineId === item.id }" @click="selectedTimelineId = item.id">
<img
:src=
"item.image"
:alt=
"item.name"
class=
"w-full h-full cover"
/>
<span
class=
"timeline-name absolute top-4 left-4 h-18 lh-18 px-4 fs-10 color-fff fw-700"
>
{{
item
.
name
}}
</span>
<span
class=
"duration absolute right-4 bottom-4 h-18 lh-18 px-4 fs-10 color-fff round-4"
>
7.0s
</span>
</div>
</div>
</div>
<span
class=
"fs-10 color-888"
>
点击画面可查看对应分镜
</span>
</section>
</div>
<footer
class=
"flex justify-end pt-12 pb-12 pr-20"
>
<el-button
type=
"primary"
class=
"fs-12 border-none create-btn"
@
click=
"emit('createVideo')"
>
生成视频成片
</el-button>
</footer>
</main>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
}
from
'vue'
;
import
{
Box
,
Crop
,
MagicStick
,
Microphone
,
Plus
,
Refresh
,
VideoCamera
,
VideoPlay
}
from
'@element-plus/icons-vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
peopleImg
from
'@/static/image/meinv.png'
;
type
PlanItem
=
{
id
:
string
;
name
:
string
;
cover
:
string
;
dot
?:
boolean
;
materials
:
MaterialItem
[];
};
type
MaterialItem
=
{
id
:
string
;
name
:
string
;
image
:
string
;
};
type
ShotDescription
=
{
time
:
string
;
scene
:
string
;
speech
:
string
;
};
const
emit
=
defineEmits
<
{
createVideo
:
[];
}
>
();
const
plans
:
PlanItem
[]
=
[
{
id
:
'recommend'
,
name
:
'推荐'
,
cover
:
productImg
,
materials
:
[
{
id
:
'material-1'
,
name
:
'商品原料'
,
image
:
productImg
},
{
id
:
'material-2'
,
name
:
'人物原料'
,
image
:
peopleImg
},
],
},
{
id
:
'backup-1'
,
name
:
'备选1'
,
cover
:
peopleImg
,
dot
:
true
,
materials
:
[
{
id
:
'material-3'
,
name
:
'场景原料'
,
image
:
peopleImg
},
{
id
:
'material-4'
,
name
:
'商品原料'
,
image
:
productImg
},
],
},
];
const
shotDescriptions
:
ShotDescription
[]
=
[
{
time
:
'[00:00-00:07]'
,
scene
:
'雨天车内第一视角,雨刮扫过后玻璃仍有大片油膜与光晕,驾驶员皱眉观察前方。'
,
speech
:
'一下雨,挡风玻璃就像蒙了一层雾,雨刮越刮反而越模糊。第三代'
,
},
{
time
:
'[00:07-00:14]'
,
scene
:
'近景展示玻璃油膜清洁擦,完成涂抹、擦拭和冲水;画面保留一半未清洁区域形成直观对比。'
,
speech
:
'用它在玻璃上轻轻擦几遍,冲水后油膜很快就被带走了。'
,
},
{
time
:
'[00:14-00:21]'
,
scene
:
'切回驾驶视角,挡风玻璃清晰通透,路面与车灯轮廓清楚,产品定格收尾。'
,
speech
:
'玻璃重新透亮,雨天和夜间开车都安心多了。'
,
},
];
const
timelineItems
=
[
{
id
:
'shot-1'
,
name
:
'画面1'
,
image
:
productImg
},
{
id
:
'shot-2'
,
name
:
'画面2'
,
image
:
peopleImg
},
{
id
:
'shot-3'
,
name
:
'画面3'
,
image
:
productImg
},
];
const
selectedPlanId
=
ref
(
'recommend'
);
const
selectedMaterialId
=
ref
(
'material-1'
);
const
selectedTimelineId
=
ref
(
'shot-1'
);
const
previewCoverErrorMap
=
ref
<
Record
<
string
,
boolean
>>
({});
const
selectedPlan
=
computed
(()
=>
plans
.
find
((
item
)
=>
item
.
id
===
selectedPlanId
.
value
)
||
plans
[
0
]);
const
selectPlan
=
(
id
:
string
)
=>
{
selectedPlanId
.
value
=
id
;
selectedMaterialId
.
value
=
selectedPlan
.
value
.
materials
[
0
]?.
id
||
''
;
};
const
handlePreviewCoverError
=
()
=>
{
previewCoverErrorMap
.
value
=
{
...
previewCoverErrorMap
.
value
,
[
selectedPlan
.
value
.
id
]:
true
,
};
};
</
script
>
<
style
scoped
lang=
"scss"
>
.step3-main
{
min-width
:
0
;
}
.timeline-panel
{
border-bottom
:
1px
solid
#dce6f2
;
border-top
:
1px
solid
#dce6f2
;
}
.plan-tabs
{
flex
:
0
0
68px
;
border-right
:
1px
solid
#dce6f2
;
background
:
#f8fafc
;
}
.plan-tab
{
width
:
48px
;
color
:
#0a1b34
;
&
.active
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
background
:
#fff
;
}
}
.notice-dot
{
top
:
-4px
;
right
:
6px
;
background
:
#18d4e8
;
}
.video-panel
{
flex
:
0
0
220px
;
border-right
:
1px
solid
#dce6f2
;
}
.preview-cover
,
.material-card
,
.timeline-card
{
background
:
#f3f6fb
;
}
.preview-placeholder
{
background
:
#f8fafc
;
}
.recommend-tag
{
background
:
#4f5b71
;
}
.play-btn
{
left
:
50%
;
top
:
50%
;
background
:
rgba
(
255
,
255
,
255
,
.9
);
transform
:
translate
(
-50%
,
-50%
);
}
.material-panel
{
min-width
:
0
;
}
.add-material
{
border
:
1px
dashed
#b9c9dd
;
background
:
#fff
;
}
.material-card
{
border
:
2px
solid
transparent
;
&
.active
{
border-color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
.ai-tag
{
background
:
rgba
(
18
,
24
,
38
,
.72
);
}
.description-box
{
background
:
#f8fafc
;
}
.section-line
{
height
:
1px
;
background
:
#dce6f2
;
}
.script-scroll
{
min-height
:
176px
;
}
.subject-icon
{
border
:
1px
solid
#dce6f2
;
background
:
#fff
;
}
.timeline-panel
{
flex
:
0
0
132px
;
}
.timeline-inner
{
margin-left
:
286px
;
}
.timeline-card
{
border
:
2px
solid
transparent
;
&
.active
{
border-color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
.timeline-name
{
background
:
rgba
(
18
,
24
,
38
,
.62
);
}
.duration
{
background
:
rgba
(
18
,
24
,
38
,
.72
);
}
.create-btn
{
background
:
#7d28ff
;
}
</
style
>
apps/web-vue/src/views/agent/components/Step4.vue
0 → 100644
View file @
048729dc
<
template
>
<main
class=
"step4-main flex-1 flex-col hidden"
>
<section
class=
"flex-1 over-y-auto hide-scroll py-20 px-20"
>
<div
class=
"video-list flex flex-wrap gap-18"
>
<div
v-for=
"item in videos"
:key=
"item.id"
class=
"video-card flex-col round-8 hidden bg-fff"
:class=
"
{ active: selectedVideoIds.includes(item.id) }">
<div
class=
"video-cover relative hidden"
>
<img
:src=
"item.cover"
:alt=
"item.name"
class=
"w-full h-full cover"
/>
<el-checkbox
:model-value=
"selectedVideoIds.includes(item.id)"
class=
"select-check absolute top-12 left-12"
@
change=
"toggleVideo(item.id)"
/>
<span
v-if=
"item.recommend"
class=
"recommend-tag absolute top-10 right-10 h-24 lh-24 px-10 round-4 fs-12 fw-700 color-fff"
>
推荐
</span>
<span
class=
"duration-tag absolute right-10 bottom-10 h-24 lh-24 px-8 round-4 fs-12 color-fff"
>
{{
item
.
duration
}}
</span>
</div>
<div
class=
"p-12"
>
<div
class=
"fs-12 fw-700 color-000 lh-18 ellipsis mb-10"
>
{{
item
.
name
}}
</div>
<div
class=
"flex"
>
<el-button
class=
"detail-btn flex-1 h-34 fs-12"
>
<el-icon>
<View
/>
</el-icon>
<span>
详情
</span>
</el-button>
<el-button
class=
"icon-btn wh-34 p-0"
>
<el-icon>
<Upload
/>
</el-icon>
</el-button>
<el-button
class=
"icon-btn wh-34 p-0"
>
<el-icon>
<Download
/>
</el-icon>
</el-button>
</div>
</div>
</div>
</div>
</section>
<footer
class=
"flex-between items-center px-20 py-12"
style=
"border-top: 1px solid #eee;"
>
<div
class=
"flex justify-end"
>
<el-checkbox
v-model=
"isAllSelected"
:indeterminate=
"isIndeterminate"
class=
"fs-12"
>
全选
</el-checkbox>
</div>
<div
class=
"flex-items-center gap-10"
>
<div
class=
"fs-12 color-888 text-right"
>
已选择
<span
class=
"color-main fw-700"
>
{{
selectedVideoIds
.
length
}}
</span>
个视频
</div>
<el-button
type=
"primary"
class=
"fs-12 border-none create-btn"
@
click=
"emit('uploadResource')"
>
上传资源库
</el-button>
</div>
</footer>
</main>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
}
from
'vue'
;
import
{
Download
,
Upload
,
View
}
from
'@element-plus/icons-vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
peopleImg
from
'@/static/image/meinv.png'
;
type
VideoItem
=
{
id
:
string
;
name
:
string
;
cover
:
string
;
duration
:
string
;
recommend
?:
boolean
;
};
const
emit
=
defineEmits
<
{
uploadResource
:
[];
}
>
();
const
videos
:
VideoItem
[]
=
[
{
id
:
'video-1'
,
name
:
'推荐版_玻璃油膜擦营销成片.mp4'
,
cover
:
productImg
,
duration
:
'00:28'
,
recommend
:
true
,
},
{
id
:
'video-2'
,
name
:
'备选1版_玻璃油膜擦营销成片.mp4'
,
cover
:
peopleImg
,
duration
:
'00:30'
,
},
];
const
selectedVideoIds
=
ref
<
string
[]
>
([
'video-1'
]);
const
isAllSelected
=
computed
({
get
:
()
=>
selectedVideoIds
.
value
.
length
===
videos
.
length
,
set
:
(
value
:
boolean
)
=>
{
selectedVideoIds
.
value
=
value
?
videos
.
map
((
item
)
=>
item
.
id
)
:
[];
},
});
const
isIndeterminate
=
computed
(()
=>
selectedVideoIds
.
value
.
length
>
0
&&
selectedVideoIds
.
value
.
length
<
videos
.
length
);
const
toggleVideo
=
(
id
:
string
)
=>
{
if
(
selectedVideoIds
.
value
.
includes
(
id
))
{
selectedVideoIds
.
value
=
selectedVideoIds
.
value
.
filter
((
item
)
=>
item
!==
id
);
return
;
}
selectedVideoIds
.
value
=
[...
selectedVideoIds
.
value
,
id
];
};
</
script
>
<
style
scoped
lang=
"scss"
>
.step4-main
{
min-width
:
0
;
}
.video-list
{
align-content
:
flex-start
;
}
.video-card
{
flex
:
0
0
260px
;
border
:
1px
solid
#dce6f2
;
transition
:
border-color
.2s
,
box-shadow
.2s
;
&
.active
{
border-color
:
rgba
(
var
(
--
main-color
)
,
1
);
box-shadow
:
0
0
0
1px
rgba
(
var
(
--
main-color
)
,
.35
);
}
}
.video-cover
{
height
:
146px
;
background
:
#f3f6fb
;
}
.recommend-tag
,
.create-btn
{
background
:
#7d28ff
;
}
.duration-tag
{
background
:
rgba
(
18
,
24
,
38
,
.78
);
}
.detail-btn
,
.icon-btn
{
border-color
:
#dce6f2
;
}
:deep
(
.select-check
.el-checkbox__label
)
{
display
:
none
;
}
:deep
(
.select-check
.el-checkbox__inner
)
{
width
:
18px
;
height
:
18px
;
border-radius
:
4px
;
}
:deep
(
.select-check
.el-checkbox__inner
::after
)
{
left
:
8px
;
top
:
7px
;
}
</
style
>
apps/web-vue/src/views/agent/index.vue
View file @
048729dc
...
...
@@ -6,6 +6,14 @@
</el-icon>
<span
class=
"fs-12 fw-600"
>
返回
</span>
</div>
<div
v-else
class=
"fixed top-20 px-10 right-10 flex-items-center gap-6 pointer"
@
click=
"historyDrawerVisible = true"
>
<el-button>
<el-icon
class=
"fs-16"
>
<AlarmClock
/>
</el-icon>
<span
class=
"fs-12 fw-600"
>
历史任务
</span>
</el-button>
</div>
<section
v-if=
"!hasMessages"
>
<div
class=
"mt-60 flex-col items-center"
>
<div
class=
"flex-center wh-44 round-8 color-fff bg-jb"
>
...
...
@@ -19,7 +27,8 @@
<el-input
v-model=
"prompt"
:rows=
"10"
type=
"textarea"
resize=
"none"
placeholder=
"发送给 Agent"
/>
<div
class=
"flex-between pt-16"
>
<AgentPromptTools
/>
<div
class=
"border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth"
@
click=
"hasMessages = true"
>
<div
class=
"border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth"
@
click=
"hasMessages = true"
>
开始创作
<div
class=
"wh-24 color-fff round flex-center bg-jb"
>
<el-icon>
...
...
@@ -31,9 +40,12 @@
</div>
<div
class=
"flex-center mt-16"
>
<div
class=
"flex-center gap-10"
>
<el-button
v-for=
"item in suggestions"
:key=
"item"
class=
"flex-center h-40 px-20 round-100 border-none boxshaw"
@
click=
"applySuggestion(item)"
>
{{
item
}}
<el-button
class=
"flex-center h-40 px-20 round-100 border-none boxshaw"
@
click=
"goProductVideo"
>
用商品拍一条营销视频
</el-button>
<el-button
class=
"flex-center h-40 px-20 round-100 border-none boxshaw"
@
click=
"applySuggestion('参考样片写分镜脚本')"
>
参考样片写分镜脚本
</el-button>
</div>
</div>
...
...
@@ -104,26 +116,25 @@
</div>
</div>
</section>
<AgentHistoryDrawer
v-model=
"historyDrawerVisible"
/>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
nextTick
,
onBeforeUnmount
,
onMounted
,
ref
}
from
'vue'
;
import
type
{
Component
}
from
'vue
'
;
import
{
A
rrowDown
,
Back
,
Box
,
Coin
,
CopyDocument
,
Link
,
MagicStick
,
Position
,
Top
}
from
'@element-plus/icons-vue'
;
import
{
useRouter
}
from
'vue-router
'
;
import
{
A
larmClock
,
ArrowDown
,
Back
,
CopyDocument
,
MagicStick
,
Top
}
from
'@element-plus/icons-vue'
;
import
UploadFile
from
'@/components/Upload/index.vue'
import
AgentHistoryDrawer
from
'./components/AgentHistoryDrawer.vue'
;
import
AgentPromptTools
from
'./components/AgentPromptTools.vue'
;
type
ToolItem
=
{
label
:
string
;
icon
:
Component
;
hasArrow
?:
boolean
;
};
const
prompt
=
ref
(
''
);
const
router
=
useRouter
();
const
submittedTopic
=
ref
(
'美颜'
);
const
messagesCount
=
ref
(
0
);
const
hasMessages
=
ref
(
false
)
const
historyDrawerVisible
=
ref
(
false
);
const
chatScrollRef
=
ref
<
HTMLElement
>
();
const
composerBoxRef
=
ref
<
HTMLElement
>
();
const
isInputFocused
=
ref
(
false
);
...
...
@@ -131,17 +142,6 @@ const isComposerActive = ref(false);
const
isNearBottom
=
ref
(
true
);
const
isComposerCompact
=
computed
(()
=>
!
isNearBottom
.
value
&&
!
isInputFocused
.
value
&&
!
isComposerActive
.
value
);
const
agentControls
:
ToolItem
[]
=
[
{
label
:
'Agent 模式'
,
icon
:
Position
,
hasArrow
:
true
},
{
label
:
'智能模型'
,
icon
:
Coin
},
{
label
:
'生成参数'
,
icon
:
MagicStick
,
hasArrow
:
true
},
{
label
:
'引用'
,
icon
:
Link
},
{
label
:
'使用 Skill'
,
icon
:
Box
},
{
label
:
'优化'
,
icon
:
MagicStick
},
];
const
suggestions
=
[
'用商品拍一条营销视频'
,
'参考样片写分镜脚本'
];
const
sendMessage
=
()
=>
{
const
value
=
prompt
.
value
.
trim
();
submittedTopic
.
value
=
value
||
submittedTopic
.
value
;
...
...
@@ -155,6 +155,10 @@ const applySuggestion = (value: string) => {
sendMessage
();
};
const
goProductVideo
=
()
=>
{
router
.
push
(
'/agent/product-video'
);
};
const
handleChatScroll
=
()
=>
{
if
(
!
chatScrollRef
.
value
)
{
return
;
...
...
@@ -339,4 +343,5 @@ onBeforeUnmount(() => {
opacity
:
1
;
}
}
</
style
>
apps/web-vue/src/views/agent/product-video.vue
0 → 100644
View file @
048729dc
<
template
>
<div
class=
"p-20 vh100 hidden"
>
<el-card
class=
"h-full round-10 content"
shadow=
"never"
>
<template
#
header
>
<div
class=
"flex-items-center"
>
<div
class=
""
>
<span
class=
"fw-600"
>
{{
title
}}
</span>
</div>
<div
class=
"flex-1 flex-center gap-10 mr-100"
>
<div
v-for=
"(item, index) in steps"
:key=
"index"
class=
"flex-items-center gap-10 default"
:class=
"
{ finish: item.stepn
<
currentStep
,
active:
item
.
stepn =
==
currentStep
,
select:
selectStep =
==
item
.
stepn
}"
@
click=
"lookStep(item)"
>
<div
class=
"flex-items-center px-10 py-6 gap-6 round-6 pointer color-888 name"
>
<el-icon>
<component
:is=
"item.icon"
/>
</el-icon>
<span>
{{
item
.
name
}}
</span>
</div>
<div
v-if=
"index
<
steps
.
length-1
"
class=
"w-40 line"
style=
"height: 1px;"
></div>
</div>
</div>
</div>
</
template
>
<div
class=
"flex h-full"
>
<Step1
v-if=
"selectStep === 1"
@
create-record=
"addCreateRecord"
/>
<Step2
v-else-if=
"selectStep === 2"
@
create-preview=
"addPreviewRecord"
/>
<Step3
v-else-if=
"selectStep === 3"
@
create-video=
"addVideoRecord"
/>
<Step4
v-else-if=
"selectStep === 4"
@
upload-resource=
"addUploadRecord"
/>
<div
v-else
class=
"flex-1 flex-center color-888 fs-14"
>
当前步骤内容待生成
</div>
<aside
class=
"record-side flex-col bg-fff"
>
<header
class=
"record-header flex-items-center gap-8 h-48 px-16"
>
<el-icon
class=
"color-main"
>
<ChatDotSquare
/>
</el-icon>
<span
class=
"fs-14 fw-700 color-000"
>
创作记录
</span>
</header>
<div
class=
"flex-1 over-y-auto px-18 py-16"
>
<div
class=
"flex justify-end mb-16"
>
<div
class=
"user-message color-fff fs-12 lh-20 px-16 py-10 round-8"
>
用我的「商品」拍一条营销视频
</div>
</div>
<div
class=
"product-mini flex-items-center gap-12 p-12 round-8 mb-20"
>
<img
:src=
"productImg"
alt=
"看看卡萨斯"
class=
"mini-cover wh-58 round-4 cover"
/>
<div>
<div
class=
"fs-12 fw-700 color-000 mb-6"
>
看看卡萨斯
</div>
<div
class=
"fs-12 color-888"
>
商品 · 共 1 张参考图
</div>
</div>
</div>
<div
class=
"assistant-text fs-12 color-333 lh-22 mb-18"
>
为了帮您制作合适的电商营销视频,请补充具体商品信息,例如商品名称、所属品类、品牌或款式。
</div>
<div
class=
"flex justify-end mb-18"
>
<div
class=
"user-message color-fff fs-12 lh-20 px-16 py-10 round-8"
>
看看卡萨斯
</div>
</div>
<div
class=
"assistant-text fs-12 color-333 lh-22 mb-14"
>
商品信息已确认,正在为您进行需求分析。
</div>
<div
class=
"record-card flex-items-center gap-12 p-12 round-10 mb-20"
>
<el-icon
class=
"color-main fs-18"
>
<Document
/>
</el-icon>
<div>
<div
class=
"fs-12 fw-700 color-000 mb-4"
>
看看卡萨斯
</div>
<div
class=
"fs-10 color-999"
>
查看详情 | 09:12
</div>
</div>
</div>
<
template
v-for=
"item in chatMessages"
:key=
"item.id"
>
<div
class=
"assistant-text fs-12 color-333 lh-22 mb-12"
>
已根据您的要求更新需求分析内容。
</div>
<div
class=
"flex justify-end mb-18"
>
<div
class=
"user-message color-fff fs-12 lh-20 px-16 py-10 round-8"
>
{{
item
.
content
}}
</div>
</div>
</
template
>
</div>
<div
class=
"chat-input-wrap p-12"
>
<div
class=
"chat-input flex-items-center gap-10 p-8 round-8"
>
<el-input
v-model=
"chatInput"
class=
"chat-text flex-1"
type=
"textarea"
resize=
"none"
:rows=
"2"
placeholder=
"卡萨丁卡上课的"
/>
<el-button
type=
"primary"
class=
"send-btn wh-36 p-0 border-none"
@
click=
"sendChatMessage"
>
<el-icon>
<Top
/>
</el-icon>
</el-button>
</div>
</div>
</aside>
</div>
</el-card>
</div>
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
}
from
'vue'
;
import
{
ChatDotSquare
,
Document
,
Edit
,
Top
}
from
'@element-plus/icons-vue'
;
import
productImg
from
'@/static/image/huaban.png'
;
import
Step1
from
'./components/Step1.vue'
;
import
Step2
from
'./components/Step2.vue'
;
import
Step3
from
'./components/Step3.vue'
;
import
Step4
from
'./components/Step4.vue'
;
type
ChatMessage
=
{
id
:
number
;
content
:
string
;
};
const
title
=
ref
(
'商品分析信息'
)
const
currentStep
=
ref
(
4
)
const
selectStep
=
ref
(
1
)
const
steps
=
ref
([
{
name
:
'需求分析'
,
icon
:
Edit
,
stepn
:
1
,
desc
:
'商品分析信息'
},
{
name
:
'创意与分镜'
,
icon
:
Edit
,
stepn
:
2
,
desc
:
'创意与分镜'
},
{
name
:
'视频预览'
,
icon
:
Edit
,
stepn
:
3
,
desc
:
'视频预览'
},
{
name
:
'视频成片'
,
icon
:
Edit
,
stepn
:
4
,
desc
:
'视频成片'
},
])
const
lookStep
=
(
item
:
any
)
=>
{
if
(
currentStep
.
value
<
item
.
stepn
)
return
selectStep
.
value
=
item
.
stepn
title
.
value
=
item
.
desc
}
const
chatMessages
=
ref
<
ChatMessage
[]
>
([
{
id
:
1
,
content
:
'大活动哈师大'
},
{
id
:
2
,
content
:
'大萨达搬还打算大萨达'
},
{
id
:
3
,
content
:
'大萨达马上到凯撒'
},
{
id
:
4
,
content
:
'大萨达久啊圣诞节啊十八大手机登记登记啊萨'
},
{
id
:
5
,
content
:
'流量卡的角度讲撒娇大萨达'
},
]);
const
chatInput
=
ref
(
''
);
const
sendChatMessage
=
()
=>
{
const
value
=
chatInput
.
value
.
trim
();
if
(
!
value
)
{
return
;
}
chatMessages
.
value
.
push
({
id
:
Date
.
now
(),
content
:
value
,
});
chatInput
.
value
=
''
;
};
const
addCreateRecord
=
()
=>
{
currentStep
.
value
=
Math
.
max
(
currentStep
.
value
,
2
);
selectStep
.
value
=
2
;
title
.
value
=
'创意与分镜'
;
chatMessages
.
value
.
push
({
id
:
Date
.
now
(),
content
:
'生成创意与分镜'
,
});
};
const
addPreviewRecord
=
()
=>
{
currentStep
.
value
=
Math
.
max
(
currentStep
.
value
,
3
);
selectStep
.
value
=
3
;
title
.
value
=
'视频预览'
;
chatMessages
.
value
.
push
({
id
:
Date
.
now
(),
content
:
'生成视频预览'
,
});
};
const
addVideoRecord
=
()
=>
{
currentStep
.
value
=
Math
.
max
(
currentStep
.
value
,
4
);
selectStep
.
value
=
4
;
title
.
value
=
'视频成片'
;
chatMessages
.
value
.
push
({
id
:
Date
.
now
(),
content
:
'生成视频成片'
,
});
};
const
addUploadRecord
=
()
=>
{
chatMessages
.
value
.
push
({
id
:
Date
.
now
(),
content
:
'上传资源库'
,
});
};
</
script
>
<
style
scoped
lang=
"scss"
>
.content
{
:deep
(
.el-card__body
)
{
padding
:
0
;
}
}
.default
{
.name
{
&
:hover
{
background-color
:
#eee
;
}
}
.line
{
background
:
#ddd
;
}
&
.finish
{
.name
{
color
:
#01b95d
;
}
.line
{
background
:
#01b95d
;
}
}
&
.active
{
.name
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
.line
{
background
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
&
.select
{
.name
{
color
:
#fff
;
background-color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
.line
{
background
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
}
.backhover
{
&
:hover
{
color
:
rgba
(
var
(
--
main-color
)
,
1
);
}
}
.record-card
,
.product-mini
{
border
:
1px
solid
#dce6f2
;
}
.send-btn
,
.user-message
{
background
:
#7d28ff
;
}
.record-side
{
flex
:
0
0
30%
;
border-left
:
1px
solid
#dce6f2
;
}
.record-header
{
border-bottom
:
1px
solid
#dce6f2
;
}
.product-mini
{
width
:
250px
;
margin-left
:
auto
;
background
:
#f4f0ff
;
}
.mini-cover
{
background
:
#f8fafc
;
}
.record-card
{
width
:
280px
;
background
:
#fbfdff
;
}
.chat-input-wrap
{
border-top
:
1px
solid
#dce6f2
;
}
.chat-input
{
border
:
1px
solid
#dce6f2
;
background
:
#fbfdff
;
}
:deep
(
.chat-text
.el-textarea__inner
)
{
padding
:
0
;
box-shadow
:
none
;
background
:
transparent
;
}
:deep
(
.chat-text
.el-textarea__inner
)
{
font-size
:
12px
;
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment