Compare commits
3 Commits
b55a5ba896
...
26cd16be09
| Author | SHA1 | Date | |
|---|---|---|---|
| 26cd16be09 | |||
| 6098993bb3 | |||
| c3d26bdb49 |
3
src/assets/icons/RegionIcon.svg
Normal file
3
src/assets/icons/RegionIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.84839 1.55138C6.37983 1.0828 5.6203 1.08288 5.15115 1.55159C4.68298 2.01933 4.68278 2.77954 5.15158 3.24883C5.61926 3.71701 6.37934 3.71723 6.84859 3.24842C7.31704 2.7804 7.31734 2.02035 6.84839 1.55138ZM4.30303 0.702658C5.24018 -0.233608 6.75921 -0.234902 7.69694 0.70287C8.63426 1.64024 8.63456 3.16037 7.69673 4.09734C7.38215 4.41162 7.00187 4.62058 6.6 4.724V9C6.6 9.33137 6.33137 9.6 6 9.6C5.66863 9.6 5.4 9.33137 5.4 9V4.72439C4.9979 4.62112 4.61738 4.41203 4.3026 4.09692C3.36652 3.15986 3.36495 1.63989 4.30303 0.702658ZM4.1891 7.30443C4.25198 7.62979 4.0392 7.94451 3.71385 8.00738C2.86688 8.17106 2.19276 8.4162 1.74744 8.68995C1.27691 8.97919 1.2 9.20648 1.2 9.3C1.2 9.36533 1.23168 9.49297 1.43046 9.67573C1.62975 9.85897 1.95124 10.0516 2.39909 10.2257C3.29098 10.5726 4.56389 10.8 6 10.8C7.43613 10.8 8.70904 10.5726 9.60092 10.2257C10.0488 10.0516 10.3703 9.85897 10.5695 9.67573C10.7683 9.49297 10.8 9.36533 10.8 9.3C10.8 9.20648 10.7231 8.9792 10.2526 8.68995C9.80723 8.4162 9.13311 8.17106 8.28615 8.00738C7.9608 7.9445 7.74802 7.62978 7.8109 7.30443C7.87378 6.97908 8.1885 6.7663 8.51385 6.82918C9.44493 7.00912 10.2708 7.29258 10.881 7.66766C11.4659 8.02723 12 8.569 12 9.3C12 9.81457 11.7295 10.2394 11.3817 10.5591C11.0345 10.8783 10.5652 11.1383 10.0359 11.3441C8.97332 11.7573 7.54623 12 6 12C4.45378 12 3.02669 11.7573 1.96416 11.3441C1.4348 11.1383 0.965481 10.8783 0.61826 10.5591C0.270528 10.2394 0 9.81457 0 9.3C0 8.569 0.534063 8.02724 1.11901 7.66766C1.72918 7.29258 2.55506 7.00912 3.48615 6.82918C3.8115 6.7663 4.12622 6.97908 4.1891 7.30443Z" fill="#FF7A51"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
7
src/assets/icons/StyleIcon.svg
Normal file
7
src/assets/icons/StyleIcon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.75 6.75C4.24706 6.75 4.65 7.15294 4.65 7.65C4.65 8.14706 4.24706 8.55 3.75 8.55C3.25294 8.55 2.85 8.14706 2.85 7.65C2.85 7.15294 3.25294 6.75 3.75 6.75Z" fill="#FF7A51"/>
|
||||
<path d="M3.3 3.9C3.79706 3.9 4.2 4.30294 4.2 4.8C4.2 5.29706 3.79706 5.7 3.3 5.7C2.80294 5.7 2.4 5.29706 2.4 4.8C2.4 4.30294 2.80294 3.9 3.3 3.9Z" fill="#FF7A51"/>
|
||||
<path d="M8.25 3.15C8.74706 3.15 9.15 3.55294 9.15 4.05C9.15 4.54706 8.74706 4.95 8.25 4.95C7.75294 4.95 7.35 4.54706 7.35 4.05C7.35 3.55294 7.75294 3.15 8.25 3.15Z" fill="#FF7A51"/>
|
||||
<path d="M5.55 2.1C6.04706 2.1 6.45 2.50294 6.45 3C6.45 3.49706 6.04706 3.9 5.55 3.9C5.05294 3.9 4.65 3.49706 4.65 3C4.65 2.50294 5.05294 2.1 5.55 2.1Z" fill="#FF7A51"/>
|
||||
<path d="M6 0C9.31371 0 12 2.68629 12 6C12 7.21801 11.0125 8.20547 9.79453 8.20547H7.42031C7.20907 8.2055 7.0377 8.37684 7.0377 8.58809C7.03773 8.67163 7.06507 8.75301 7.11563 8.81953L7.65703 9.53145C7.83089 9.76021 7.92534 10.0398 7.92539 10.3271C7.92539 11.2509 7.1763 12 6.25254 12H6C2.68629 12 0 9.31371 0 6C4.83208e-08 2.68629 2.68629 4.83193e-08 6 0ZM6 1.2C3.34903 1.2 1.2 3.34903 1.2 6C1.2 8.65097 3.34903 10.8 6 10.8H6.25254C6.51356 10.8 6.72539 10.5882 6.72539 10.3271C6.72534 10.3022 6.71699 10.2779 6.70195 10.258L6.16055 9.54551C5.9513 9.27019 5.83773 8.93389 5.8377 8.58809C5.8377 7.7141 6.54633 7.0055 7.42031 7.00547H9.79453C10.3498 7.00547 10.8 6.55527 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2Z" fill="#FF7A51"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10
src/assets/icons/TypeIcon.svg
Normal file
10
src/assets/icons/TypeIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5159_47499)">
|
||||
<path d="M0 5.49996H5.49996V0H0V5.49996ZM0.999961 0.999961H4.5V4.5H0.999961V0.999961ZM0 12H5.49996V6.50004H0V12ZM0.999961 7.5H4.5V11H0.999961V7.5ZM12 2.75004C12 1.23363 10.7664 0 9.24996 0C7.73355 0 6.50004 1.23363 6.50004 2.75004V5.50008H9.25008C10.7664 5.49996 12 4.26633 12 2.75004ZM7.5 2.75004C7.5 1.78512 8.28504 1.00008 9.24996 1.00008C10.2149 1.00008 10.9999 1.78512 10.9999 2.75004C10.9999 3.71496 10.215 4.5 9.24996 4.5H7.5V2.75004ZM6.50004 12H12V6.50004H6.50004V12ZM7.5 7.5H11V11H7.5V7.5Z" fill="#FF7A51"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5159_47499">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 768 B |
@@ -1,172 +0,0 @@
|
||||
# Input 组件文档
|
||||
|
||||
## 组件概述
|
||||
|
||||
`Input.vue` 是一个输入框组件,用于家具设计项目的创建和AI对话场景。主要提供文本输入、图片上传、选项选择和项目创建等功能。
|
||||
|
||||
## 组件位置
|
||||
|
||||
`src/views/home/components/Input.vue`
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `isAgentMode` | `boolean` | `false` | 是否为Agent模式(简化界面) |
|
||||
| `generating` | `boolean` | `false` | 是否正在生成内容 |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| `send` | `{ text: string, images: string[], tempImages: Array }` | 发送消息/创建项目 |
|
||||
| `pause` | - | 暂停生成 |
|
||||
|
||||
## 组件模式
|
||||
|
||||
### 普通模式 (isAgentMode = false)
|
||||
|
||||
完整的输入界面,包含以下功能:
|
||||
- 图片上传附件按钮
|
||||
- 类型选择下拉框 (Sofa/Desk/Chair)
|
||||
- 区域选择下拉框
|
||||
- 风格选择弹窗 (15种风格)
|
||||
- 设置弹窗 (3个滑块参数)
|
||||
- 创建项目按钮
|
||||
|
||||
### Agent模式 (isAgentMode = true)
|
||||
|
||||
简化界面,仅包含:
|
||||
- 文本输入区域
|
||||
- 发送/暂停按钮
|
||||
|
||||
## 功能详解
|
||||
|
||||
### 1. 图片上传
|
||||
|
||||
- 支持通过附件按钮或拖拽上传图片
|
||||
- 图片预览区域显示已上传图片
|
||||
- 可删除单个图片
|
||||
- 图片通过 `uploadImage` API 上传
|
||||
|
||||
### 2. 富文本编辑
|
||||
|
||||
使用 `contenteditable` div 实现:
|
||||
- 支持粘贴纯文本
|
||||
- 支持回车发送/创建
|
||||
- 支持退格键删除标签
|
||||
|
||||
### 3. 热点报告标签
|
||||
|
||||
点击底部按钮添加特殊标签:
|
||||
- 标签包含图标和文本
|
||||
- 可通过关闭按钮删除
|
||||
- 插入到编辑器光标位置
|
||||
|
||||
### 4. 风格选择弹窗
|
||||
|
||||
15种家具风格可选:
|
||||
```
|
||||
Venetian Modern, Coastal, Maximalism, Memphis, Verdant,
|
||||
Century Chrome, Modern Revival, Transitional, Tuscan 2000's,
|
||||
Kitsch-core, Bauhaus, Constructivism, Nordic Noir, Dopamine, Squiggle
|
||||
```
|
||||
|
||||
### 5. 设置弹窗
|
||||
|
||||
3个滑块参数配置 (默认值50%):
|
||||
- 对应翻译路径: `Input.settingOptions.first/second/third`
|
||||
|
||||
## 关键方法
|
||||
|
||||
| 方法名 | 作用域 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `triggerFileUpload` | 公开 | 触发文件选择 |
|
||||
| `handleFileChange` | 私有 | 处理文件选择变化 |
|
||||
| `removeImage` | 私有 | 删除指定索引的图片 |
|
||||
| `handleEditorInput` | 私有 | 处理编辑器输入 |
|
||||
| `handleEditorPaste` | 私有 | 处理粘贴事件 |
|
||||
| `handleKeyDown` | 私有 | 处理键盘事件 |
|
||||
| `handleSendAgent` | 私有 | Agent模式发送 |
|
||||
| `handleCreateProject` | 私有 | 创建项目 |
|
||||
| `addReportTag` | 公开 | 添加热点报告标签 |
|
||||
| `toogltReportTag` | 私有 | 切换热点报告标签 |
|
||||
| `selectStyle` | 私有 | 选择风格 |
|
||||
| `confirmStyle` | 私有 | 确认风格选择 |
|
||||
| `confirmSetting` | 私有 | 确认设置 |
|
||||
|
||||
## 公开API (defineExpose)
|
||||
|
||||
```typescript
|
||||
defineExpose({
|
||||
addReportTag: (text?: string) => void
|
||||
})
|
||||
```
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 上传图片格式
|
||||
```typescript
|
||||
interface UploadedImage {
|
||||
url: string // 预览URL (base64)
|
||||
name: string // 文件名
|
||||
path: string // 服务器路径
|
||||
}
|
||||
```
|
||||
|
||||
### 发送Payload格式
|
||||
```typescript
|
||||
interface SendPayload {
|
||||
text: string // 输入文本
|
||||
images: string[] // 图片服务器路径
|
||||
tempImages: UploadedImage[] // 预览图片数据
|
||||
}
|
||||
```
|
||||
|
||||
### 创建项目参数
|
||||
```typescript
|
||||
interface ProjectParams {
|
||||
type: string // 家具类型
|
||||
area: string // 区域
|
||||
style: string // 风格
|
||||
temperature: number // 温度参数 (固定0.7)
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖项
|
||||
|
||||
- **Vue API**: `computed`, `ref`, `watch`, `nextTick`, `onMounted`
|
||||
- **Store**: `useAgentStore`, `useProjectStore`
|
||||
- **Utils**: `areaList` (区域列表), `getStyleImage` (风格图片)
|
||||
- **API**: `createProject`, `uploadImage`
|
||||
- **UI库**: Element Plus (`el-select`, `el-popover`, `el-slider`)
|
||||
- **国际化**: `vue-i18n`
|
||||
|
||||
## 样式说明
|
||||
|
||||
- 使用 `<style lang="less">` scoped 样式
|
||||
- 全局样式部分使用非 scoped (`.fida-` 前缀类名)
|
||||
- 支持 hover 动画效果
|
||||
- Agent 模式下样式有所简化
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 编辑器使用 `contenteditable`,需手动处理输入事件
|
||||
2. 图片上传后使用 FileReader 读取本地预览
|
||||
3. 标签使用 DOM 操作动态插入
|
||||
4. 项目创建后会路由跳转至 `/home/agent/{projectId}`
|
||||
5. 组件会自动聚焦编辑器
|
||||
|
||||
## 国际化键名参考
|
||||
|
||||
- `Input.placeholder` - 输入框占位符
|
||||
- `Input.typePlaceholder` - 类型选择占位符
|
||||
- `Input.areaPlaceholder` - 区域选择占位符
|
||||
- `Input.stylePlaceholder` - 风格选择占位符
|
||||
- `Input.chooseStyle` - 选择风格标题
|
||||
- `Input.confirm` - 确认按钮
|
||||
- `Input.styleTitle` - 设置弹窗标题
|
||||
- `Input.createProject` - 创建项目按钮
|
||||
- `Input.trendingReport` - 热点报告按钮文本
|
||||
- `Input.types.sofa/desk/chair` - 家具类型选项
|
||||
- `Input.settingOptions.first/second/third` - 设置选项
|
||||
File diff suppressed because it is too large
Load Diff
99
src/views/home/components/input/AgentOperatePopover.vue
Normal file
99
src/views/home/components/input/AgentOperatePopover.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="agent-operate flex flex-center">
|
||||
<el-popover placement="top" trigger="click" popper-class="agent-plus-popover">
|
||||
<template #reference>
|
||||
<SvgIcon name="plus" color="#0D0D0D" size="16" />
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="agent-modal flex flex-col">
|
||||
<div class="file flex align-center" @click="emit('upload')">
|
||||
<img src="@/assets/icons/attach.svg" class="file-icon" />
|
||||
<span>Upload files</span>
|
||||
</div>
|
||||
<div class="gap"></div>
|
||||
<div class="report flex align-center" @click="emit('toggle-report')">
|
||||
<SvgIcon color="#5A5A5A" name="light" size="11" />
|
||||
<span>Trending report</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: 'upload'): void
|
||||
(e: 'toggle-report'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.agent-operate {
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.agent-plus-popover {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
margin-bottom: -1rem;
|
||||
width: fit-content !important;
|
||||
min-width: initial !important;
|
||||
|
||||
.el-popper__arrow:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-modal {
|
||||
row-gap: 1.2rem;
|
||||
font-family: 'Medium';
|
||||
font-weight: 500;
|
||||
font-size: 1.3rem;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
box-shadow: 0px 6.53px 32.63px 0px #0000000d;
|
||||
border-radius: 1rem;
|
||||
padding: 1.2rem 1.4rem;
|
||||
transform: translateX(calc(50% - 1.6rem));
|
||||
|
||||
.c-svg {
|
||||
width: initial;
|
||||
width: 1rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
.file,
|
||||
.report {
|
||||
line-height: 1.8rem;
|
||||
column-gap: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file {
|
||||
.file-icon {
|
||||
width: 1.1rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gap {
|
||||
height: 0.05rem;
|
||||
background-color: #d4d4d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
231
src/views/home/components/input/InputEditor.vue
Normal file
231
src/views/home/components/input/InputEditor.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="editor-wrapper" :class="{ agent: isAgentMode }">
|
||||
<div v-if="showPlaceholder" class="editor-placeholder">
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
<div
|
||||
ref="editorRef"
|
||||
class="editor"
|
||||
contenteditable="true"
|
||||
@input="emit('input')"
|
||||
@paste="emit('paste', $event)"
|
||||
@keydown="emit('keydown', $event)"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
showPlaceholder?: boolean
|
||||
placeholder: string
|
||||
isAgentMode?: boolean
|
||||
}>(),
|
||||
{
|
||||
showPlaceholder: false,
|
||||
isAgentMode: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'ready', element: HTMLDivElement | null): void
|
||||
(e: 'input'): void
|
||||
(e: 'paste', event: ClipboardEvent): void
|
||||
(e: 'keydown', event: KeyboardEvent): void
|
||||
}>()
|
||||
|
||||
const editorRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
const focus = () => {
|
||||
editorRef.value?.focus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emit('ready', editorRef.value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
editorRef,
|
||||
focus
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editor-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
|
||||
.editor-placeholder {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 1.4rem 1.4rem;
|
||||
font-size: 2rem;
|
||||
font-family: 'InterRegular';
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
width: calc(100% - 2.8rem);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0 1.4rem 1.4rem;
|
||||
font-size: 1.8rem;
|
||||
font-family: 'InterRegular';
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
line-height: 2.8rem;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb,
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.agent {
|
||||
.editor {
|
||||
font-family: 'Regular';
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
min-height: 5rem;
|
||||
max-height: initial;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
|
||||
.editor-placeholder {
|
||||
font-family: 'Regular';
|
||||
font-size: 1.4rem;
|
||||
padding: 0;
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.assist-input-wrapper .editor .editor-tag {
|
||||
width: 15.6rem;
|
||||
height: 3.1rem;
|
||||
display: inline-flex;
|
||||
border: 1px solid #0000001a;
|
||||
font-weight: 500;
|
||||
font-size: 1.3rem;
|
||||
column-gap: 0;
|
||||
margin: 0 0.5rem;
|
||||
vertical-align: middle;
|
||||
border-radius: 2.2rem;
|
||||
|
||||
&.restore {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.9rem 0 0.7rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.option-tag {
|
||||
width: auto;
|
||||
max-width: 24rem;
|
||||
height: 3.4rem;
|
||||
padding: 0 1rem;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
column-gap: 0.8rem;
|
||||
color: #0d0d0d;
|
||||
|
||||
.option-tag-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-tag-text {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.option-close {
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 0.7rem 0 1.2rem;
|
||||
|
||||
&.restore-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 52rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.light-icon {
|
||||
width: 0.9rem;
|
||||
height: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.restore {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.restore-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
src/views/home/components/input/InputImagePreviewList.vue
Normal file
89
src/views/home/components/input/InputImagePreviewList.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div v-if="previewItems.length > 0" class="image-preview-list flex wrap">
|
||||
<div
|
||||
v-for="(image, index) in previewItems"
|
||||
:key="`${index}-${getPreviewUrl(image)}`"
|
||||
class="image-preview-item"
|
||||
>
|
||||
<img
|
||||
:src="getPreviewUrl(image)"
|
||||
class="preview-image"
|
||||
@click="emit('preview', getPreviewUrl(image))"
|
||||
/>
|
||||
<div class="image-remove-btn" @click="emit('remove', index, image)">
|
||||
<SvgIcon name="delete" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PreviewImage, UploadedImage } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
uploadedImages: UploadedImage[]
|
||||
quoteList: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'preview', url: string): void
|
||||
(e: 'remove', index: number, image: PreviewImage): void
|
||||
}>()
|
||||
|
||||
const previewItems = computed<PreviewImage[]>(() => [
|
||||
...props.uploadedImages,
|
||||
...props.quoteList
|
||||
])
|
||||
|
||||
const getPreviewUrl = (image: PreviewImage) => {
|
||||
return typeof image === 'string' ? image : image.url
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.image-preview-list {
|
||||
padding: 0 1.4rem 1rem;
|
||||
column-gap: 1rem;
|
||||
max-height: 15rem;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
|
||||
.image-preview-item {
|
||||
position: relative;
|
||||
width: 8.6rem;
|
||||
height: 8.6rem;
|
||||
border-radius: 1.5rem;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border: 0.1rem solid #cdcdcd;
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-remove-btn {
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
right: 0.6rem;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .image-remove-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
src/views/home/components/input/InputToolbar.vue
Normal file
270
src/views/home/components/input/InputToolbar.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="operate input-toolbar flex align-center space-between" :class="{ agent: isAgentMode }">
|
||||
<div class="left flex align-center">
|
||||
<AgentOperatePopover
|
||||
v-if="isAgentMode"
|
||||
@upload="triggerFileUpload"
|
||||
@toggle-report="emit('toggle-report')"
|
||||
/>
|
||||
<div
|
||||
v-if="!isAgentMode"
|
||||
class="attach flex flex-center"
|
||||
@click="triggerFileUpload"
|
||||
>
|
||||
<img src="@/assets/icons/attach.svg" />
|
||||
</div>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="emit('file-change', $event)"
|
||||
/>
|
||||
<el-select
|
||||
v-if="!isAgentMode"
|
||||
v-model="typeModel"
|
||||
:placeholder="typePlaceholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
class="input-option"
|
||||
:key="item.value"
|
||||
:label="translate(item.label)"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="!isAgentMode"
|
||||
v-model="areaModel"
|
||||
:placeholder="areaPlaceholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in areaOptions"
|
||||
class="input-option"
|
||||
:key="item.value"
|
||||
:label="translate(item.label)"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<StyleSelect
|
||||
v-if="!isAgentMode"
|
||||
v-model="styleModel"
|
||||
:type-value="typeValue"
|
||||
:options="styleOptions"
|
||||
:placeholder="stylePlaceholder"
|
||||
:title="styleTitle"
|
||||
:confirm-text="confirmText"
|
||||
/>
|
||||
<SettingPopover
|
||||
v-model:options="settingOptionsModel"
|
||||
:title="settingTitle"
|
||||
:translate="translate"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div
|
||||
v-if="!isAgentMode"
|
||||
class="create-btn flex flex-center"
|
||||
@click="emit('create')"
|
||||
>
|
||||
<img src="@/assets/images/shining.png" class="shining-icon" alt="" />
|
||||
<span class="create-btn-text">{{ createText }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="sender-btn flex flex-center" @click="emit('send')">
|
||||
<img
|
||||
v-show="!generating"
|
||||
src="@/assets/images/sender.png"
|
||||
alt=""
|
||||
class="sender-icon"
|
||||
/>
|
||||
<div v-show="generating" class="sender-pause" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import AgentOperatePopover from './AgentOperatePopover.vue'
|
||||
import SettingPopover from './SettingPopover.vue'
|
||||
import StyleSelect from './StyleSelect.vue'
|
||||
import type { OptionItem, SettingOption } from './types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isAgentMode?: boolean
|
||||
generating?: boolean
|
||||
typeValue: string
|
||||
areaValue: string
|
||||
styleValue: string
|
||||
typeOptions: OptionItem[]
|
||||
areaOptions: OptionItem[]
|
||||
styleOptions: OptionItem[]
|
||||
settingOptions: SettingOption[]
|
||||
typePlaceholder: string
|
||||
areaPlaceholder: string
|
||||
stylePlaceholder: string
|
||||
styleTitle: string
|
||||
settingTitle: string
|
||||
confirmText: string
|
||||
createText: string
|
||||
translate: (key: string) => string
|
||||
}>(),
|
||||
{
|
||||
isAgentMode: false,
|
||||
generating: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:typeValue', value: string): void
|
||||
(e: 'update:areaValue', value: string): void
|
||||
(e: 'update:styleValue', value: string): void
|
||||
(e: 'update:settingOptions', value: SettingOption[]): void
|
||||
(e: 'file-change', event: Event): void
|
||||
(e: 'toggle-report'): void
|
||||
(e: 'create'): void
|
||||
(e: 'send'): void
|
||||
}>()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const typeModel = computed({
|
||||
get: () => props.typeValue,
|
||||
set: (value: string) => emit('update:typeValue', value)
|
||||
})
|
||||
|
||||
const areaModel = computed({
|
||||
get: () => props.areaValue,
|
||||
set: (value: string) => emit('update:areaValue', value)
|
||||
})
|
||||
|
||||
const styleModel = computed({
|
||||
get: () => props.styleValue,
|
||||
set: (value: string) => emit('update:styleValue', value)
|
||||
})
|
||||
|
||||
const settingOptionsModel = computed({
|
||||
get: () => props.settingOptions,
|
||||
set: (value: SettingOption[]) => emit('update:settingOptions', value)
|
||||
})
|
||||
|
||||
const triggerFileUpload = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.input-toolbar {
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
padding: 0 1.7rem 1.7rem;
|
||||
|
||||
.left {
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
.attach {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 1.65rem;
|
||||
height: 1.86rem;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 13.9rem;
|
||||
height: 4rem;
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
border-radius: 0.8rem;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
border: 0.1rem solid rgba(0, 0, 0, 0.1);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
min-height: initial;
|
||||
|
||||
.el-select__placeholder {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-select__icon {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background-color: #ff7a51;
|
||||
height: 4rem;
|
||||
width: 13rem;
|
||||
color: #fff;
|
||||
border-radius: 4.2rem;
|
||||
font-family: 'SemiBold';
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
column-gap: 0.3rem;
|
||||
|
||||
.shining-icon {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.agent {
|
||||
padding: 1.2rem 0 0;
|
||||
margin: 0;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sender-btn {
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
cursor: pointer;
|
||||
background-color: #ff7a51;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8693d;
|
||||
}
|
||||
|
||||
.sender-icon {
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
.sender-pause {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.input-option {
|
||||
padding-left: 2rem !important;
|
||||
color: #0d0d0d;
|
||||
font-weight: 510;
|
||||
font-size: 1.3rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
|
||||
&.el-select-dropdown__item.is-hovering {
|
||||
background-color: rgba(13, 13, 13, 0.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/views/home/components/input/ReportShortcutButton.vue
Normal file
46
src/views/home/components/input/ReportShortcutButton.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div
|
||||
class="report-btn flex space-between align-center outer"
|
||||
:class="{ 'is-cn': isCn }"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<SvgIcon class="light-icon" color="#FFDB56" name="light" size="16" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
isCn: boolean
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.report-btn {
|
||||
position: absolute;
|
||||
bottom: -7.4rem;
|
||||
height: 4.4rem;
|
||||
border-radius: 2.2rem;
|
||||
width: 19.7rem;
|
||||
padding: 0 2rem;
|
||||
font-size: 1.8rem;
|
||||
background-color: #fff;
|
||||
border: 1.1px solid #f6f4ef1a;
|
||||
cursor: pointer;
|
||||
|
||||
&.outer.is-cn {
|
||||
justify-content: center;
|
||||
column-gap: 3rem;
|
||||
}
|
||||
|
||||
.c-svg {
|
||||
width: 1.5rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
160
src/views/home/components/input/SettingPopover.vue
Normal file
160
src/views/home/components/input/SettingPopover.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<el-popover
|
||||
v-model:visible="settingPopupVisible"
|
||||
placement="top"
|
||||
:width="342"
|
||||
:show-arrow="false"
|
||||
trigger="click"
|
||||
popper-class="fida-setting-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<img src="@/assets/images/setting.png" class="setting-icon" />
|
||||
</template>
|
||||
<div class="fida-setting-popover-content flex flex-col">
|
||||
<div class="fida-setting-popover-header">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="fida-setting-slider-list">
|
||||
<div
|
||||
v-for="(item, index) in options"
|
||||
:key="item.label"
|
||||
class="fida-setting-slider-item"
|
||||
>
|
||||
<div class="fida-slider-label">{{ translate(item.label) }}</div>
|
||||
<div class="fida-slider-row flex align-center">
|
||||
<el-slider
|
||||
class="setting-popover-slider"
|
||||
:model-value="item.value"
|
||||
:show-tooltip="false"
|
||||
@update:model-value="updateOptionValue(index, $event)"
|
||||
/>
|
||||
<span class="fida-slider-value">{{ item.value }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { SettingOption } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
options: SettingOption[]
|
||||
title: string
|
||||
translate: (key: string) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:options', value: SettingOption[]): void
|
||||
}>()
|
||||
|
||||
const settingPopupVisible = ref(false)
|
||||
|
||||
const updateOptionValue = (index: number, value: number | number[]) => {
|
||||
const nextValue = Array.isArray(value) ? value[0] : value
|
||||
const nextOptions = props.options.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, value: nextValue } : item
|
||||
)
|
||||
emit('update:options', nextOptions)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-icon {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.fida-setting-popover {
|
||||
padding: 0 !important;
|
||||
border-radius: 0.6rem !important;
|
||||
background-color: #fff !important;
|
||||
border: none !important;
|
||||
width: 25.6rem;
|
||||
height: 23.9rem;
|
||||
box-shadow: 0px 11px 20px 0px #0000001a;
|
||||
}
|
||||
|
||||
.fida-setting-popover-header {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #000;
|
||||
margin-bottom: 2rem !important;
|
||||
}
|
||||
|
||||
.fida-setting-popover-content {
|
||||
padding: 1.6rem 1.4rem 2.2rem !important;
|
||||
}
|
||||
|
||||
.fida-setting-slider-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.fida-setting-slider-item {
|
||||
.fida-slider-label {
|
||||
font-weight: 400;
|
||||
font-size: 1.2rem;
|
||||
color: #000;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.fida-slider-row {
|
||||
column-gap: 2.6rem;
|
||||
|
||||
.el-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fida-slider-value {
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-popover-slider {
|
||||
--el-slider-height: 0.4rem;
|
||||
height: fit-content;
|
||||
|
||||
.el-slider__runway {
|
||||
height: var(--el-slider-height);
|
||||
background-color: #e8e8e8;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.el-slider__bar {
|
||||
height: var(--el-slider-height);
|
||||
background-color: #000;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.el-slider__button-wrapper {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-slider__button {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-slider__stop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
src/views/home/components/input/StyleSelect.vue
Normal file
224
src/views/home/components/input/StyleSelect.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="fida-style-select-wrapper">
|
||||
<el-select
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@focus="openStylePopup"
|
||||
/>
|
||||
|
||||
<el-popover
|
||||
v-model:visible="stylePopupVisible"
|
||||
placement="top"
|
||||
:width="342"
|
||||
:show-arrow="false"
|
||||
trigger="click"
|
||||
popper-class="fida-style-select-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="fida-style-select-trigger"></div>
|
||||
</template>
|
||||
<div class="fida-style-popover-content flex flex-col">
|
||||
<div class="fida-style-popover-header">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="fida-style-popover-grid">
|
||||
<div
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
class="fida-style-popover-item flex flex-center"
|
||||
:class="{ 'is-selected': tempSelectedValue === item.value }"
|
||||
@click="selectStyle(item.value)"
|
||||
>
|
||||
<img :src="getStyleImage(typeValue, item.value)" class="style-bg" />
|
||||
<span class="fida-option-label flex flex-center">{{ item.label }}</span>
|
||||
<img
|
||||
v-show="tempSelectedValue === item.value"
|
||||
src="@/assets/images/checked.png"
|
||||
class="checked-item-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="fida-style-popover-footer flex flex-center">
|
||||
<button class="fida-confirm-btn" @click="confirmStyle">
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getStyleImage } from '../style'
|
||||
import type { OptionItem } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
typeValue: string
|
||||
options: OptionItem[]
|
||||
placeholder: string
|
||||
title: string
|
||||
confirmText: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}>()
|
||||
|
||||
const tempSelectedValue = ref('')
|
||||
const stylePopupVisible = ref(false)
|
||||
|
||||
const openStylePopup = () => {
|
||||
tempSelectedValue.value = props.modelValue
|
||||
stylePopupVisible.value = true
|
||||
}
|
||||
|
||||
const selectStyle = (value: string) => {
|
||||
tempSelectedValue.value = value
|
||||
confirmStyle()
|
||||
}
|
||||
|
||||
const confirmStyle = () => {
|
||||
emit('update:modelValue', tempSelectedValue.value)
|
||||
stylePopupVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.fida-style-select-wrapper {
|
||||
position: relative;
|
||||
width: 13.9rem;
|
||||
height: 4rem;
|
||||
|
||||
.el-select {
|
||||
width: 13.9rem;
|
||||
height: 4rem;
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
border-radius: 0.8rem;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
border: 0.1rem solid rgba(0, 0, 0, 0.1);
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
min-height: initial;
|
||||
|
||||
.el-select__placeholder {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-select__icon {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fida-style-select-trigger {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.fida-style-select-popover {
|
||||
width: 34.2rem !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 0.6rem !important;
|
||||
box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.15) !important;
|
||||
background-color: #fff !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.fida-style-popover-content {
|
||||
padding: 2rem 2.4rem 2.4rem;
|
||||
}
|
||||
|
||||
.fida-style-popover-header {
|
||||
font-weight: 500;
|
||||
font-size: 1.6rem;
|
||||
color: #000;
|
||||
margin-bottom: 2rem;
|
||||
padding: 2rem 2.4rem !important;
|
||||
}
|
||||
|
||||
.fida-style-popover-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
height: 28.5rem;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
|
||||
.fida-style-popover-item {
|
||||
height: 9.1rem;
|
||||
width: 9.1rem;
|
||||
border-radius: 1.4rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: none;
|
||||
|
||||
.checked-item-icon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, 50%);
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.style-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 1.4rem;
|
||||
}
|
||||
|
||||
.fida-option-label {
|
||||
font-weight: 500;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 1.4rem;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border: 0.3rem solid #000;
|
||||
|
||||
.fida-option-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fida-style-popover-footer {
|
||||
padding: 2.4rem 0 !important;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
.fida-confirm-btn {
|
||||
margin: 0 auto;
|
||||
width: 15.7rem;
|
||||
height: 3.4rem;
|
||||
line-height: 3.4rem;
|
||||
background-color: #ff7a51;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 3.8rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
src/views/home/components/input/options.ts
Normal file
48
src/views/home/components/input/options.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { OptionItem, SettingOption } from './types'
|
||||
|
||||
export const styleKeys: string[] = [
|
||||
'Venetian Modern',
|
||||
'Coastal',
|
||||
'Maximalism',
|
||||
'Memphis',
|
||||
'Verdant',
|
||||
'Century Chrome',
|
||||
'Modern Revival',
|
||||
'Transitional',
|
||||
"Tuscan 2000's",
|
||||
'Kitsch-core',
|
||||
'Bauhaus',
|
||||
'Constructivism',
|
||||
'Nordic Noir',
|
||||
'Dopamine',
|
||||
'Squiggle'
|
||||
]
|
||||
|
||||
export const optionTagOrder = ['type', 'area', 'style'] as const
|
||||
|
||||
export const createTypeOptions = (): OptionItem[] => [
|
||||
{
|
||||
label: 'Input.types.sofa',
|
||||
value: 'Sofa'
|
||||
},
|
||||
{
|
||||
label: 'Input.types.desk',
|
||||
value: 'Desk'
|
||||
},
|
||||
{
|
||||
label: 'Input.types.chair',
|
||||
value: 'Chair'
|
||||
}
|
||||
]
|
||||
|
||||
export const createStyleOptions = (): OptionItem[] =>
|
||||
styleKeys.map((key) => ({
|
||||
label: key,
|
||||
value: key
|
||||
}))
|
||||
|
||||
export const createSettingOptions = (): SettingOption[] => [
|
||||
{ label: 'Input.settingOptions.first', value: 50 },
|
||||
{ label: 'Input.settingOptions.second', value: 50 },
|
||||
{ label: 'Input.settingOptions.third', value: 50 }
|
||||
]
|
||||
19
src/views/home/components/input/types.ts
Normal file
19
src/views/home/components/input/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface OptionItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface SettingOption {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface UploadedImage {
|
||||
url: string
|
||||
name: string
|
||||
path?: any
|
||||
}
|
||||
|
||||
export type PreviewImage = UploadedImage | string
|
||||
|
||||
export type OptionTagKind = 'type' | 'area' | 'style'
|
||||
527
src/views/home/components/input/useInputEditor.ts
Normal file
527
src/views/home/components/input/useInputEditor.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import lightIcon from '@/assets/images/light-icon.png'
|
||||
import closeIcon from '@/assets/images/close-icon.png'
|
||||
import TypeIcon from '@/assets/icons/TypeIcon.svg'
|
||||
import RegionIcon from '@/assets/icons/RegionIcon.svg'
|
||||
import StyleIcon from '@/assets/icons/StyleIcon.svg'
|
||||
import restoreIcon from '@/assets/images/restore.png'
|
||||
import restoreCloseIcon from '@/assets/images/tag-close.png'
|
||||
import { optionTagOrder } from './options'
|
||||
import type { OptionItem, OptionTagKind } from './types'
|
||||
|
||||
interface UseInputEditorOptions {
|
||||
isAgentMode: Ref<boolean> | ComputedRef<boolean>
|
||||
t: (key: string) => string
|
||||
typeValue: Ref<string>
|
||||
areaValue: Ref<string>
|
||||
styleValue: Ref<string>
|
||||
typeOptions: Ref<OptionItem[]>
|
||||
areaOptions: Ref<OptionItem[]>
|
||||
styleOptions: Ref<OptionItem[]>
|
||||
onSubmit: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export function useInputEditor(options: UseInputEditorOptions) {
|
||||
const editorRef = ref<HTMLDivElement | null>(null)
|
||||
const inputValue = ref<string>('')
|
||||
const reportTags = ref<HTMLElement[]>([])
|
||||
const optionTags = ref<Partial<Record<OptionTagKind, HTMLElement>>>({})
|
||||
const reportPromptText = ref<Text | null>(null)
|
||||
let reportTypewriterTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const setEditorElement = (element: HTMLDivElement | null) => {
|
||||
editorRef.value = element
|
||||
}
|
||||
|
||||
const focusEditor = () => {
|
||||
editorRef.value?.focus()
|
||||
}
|
||||
|
||||
const stopReportTypewriter = () => {
|
||||
if (reportTypewriterTimeout) {
|
||||
clearTimeout(reportTypewriterTimeout)
|
||||
reportTypewriterTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
const moveCaretToTextEnd = (textNode: Text) => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.anchorNode !== textNode) return
|
||||
|
||||
const range = document.createRange()
|
||||
range.setStart(textNode, textNode.data.length)
|
||||
range.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
const typeReportPromptText = (textNode: Text, text: string, index = 0) => {
|
||||
if (reportPromptText.value !== textNode) return
|
||||
|
||||
if (index >= text.length) {
|
||||
reportTypewriterTimeout = null
|
||||
return
|
||||
}
|
||||
|
||||
textNode.textContent = `${textNode.textContent || ''}${text.charAt(index)}`
|
||||
handleEditorInput()
|
||||
moveCaretToTextEnd(textNode)
|
||||
|
||||
reportTypewriterTimeout = setTimeout(() => {
|
||||
typeReportPromptText(textNode, text, index + 1)
|
||||
}, 30)
|
||||
}
|
||||
|
||||
const removeReportPromptText = () => {
|
||||
stopReportTypewriter()
|
||||
const promptText = reportPromptText.value
|
||||
if (promptText?.parentNode) {
|
||||
const nextNode = promptText.nextSibling
|
||||
if (
|
||||
nextNode &&
|
||||
nextNode.nodeType === Node.TEXT_NODE &&
|
||||
nextNode.textContent === '\u200B'
|
||||
) {
|
||||
nextNode.remove()
|
||||
}
|
||||
promptText.parentNode.removeChild(promptText)
|
||||
}
|
||||
reportPromptText.value = null
|
||||
}
|
||||
|
||||
const removeNodeWithNextSpacer = (node: Node) => {
|
||||
const nextNode = node.nextSibling
|
||||
if (nextNode && nextNode.nodeType === Node.TEXT_NODE && nextNode.textContent === '\u200B') {
|
||||
nextNode.remove()
|
||||
}
|
||||
node.parentNode?.removeChild(node)
|
||||
}
|
||||
|
||||
const hasOptionTags = () => {
|
||||
return Object.values(optionTags.value).some((tag) => tag?.parentNode)
|
||||
}
|
||||
|
||||
const getOptionTagValue = (kind: OptionTagKind) => {
|
||||
if (kind === 'type') return options.typeValue.value
|
||||
if (kind === 'area') return options.areaValue.value
|
||||
return options.styleValue.value
|
||||
}
|
||||
|
||||
const getOptionTagIcon = (kind: OptionTagKind) => {
|
||||
if (kind === 'type') return TypeIcon
|
||||
if (kind === 'area') return RegionIcon
|
||||
return StyleIcon
|
||||
}
|
||||
|
||||
const getTranslatedOptionLabel = (items: OptionItem[], value: string) => {
|
||||
const option = items.find((item) => item.value === value)
|
||||
return option ? options.t(option.label) : value
|
||||
}
|
||||
|
||||
const getOptionTagLabel = (kind: OptionTagKind) => {
|
||||
const value = getOptionTagValue(kind)
|
||||
if (kind === 'type') return getTranslatedOptionLabel(options.typeOptions.value, value)
|
||||
if (kind === 'area') return getTranslatedOptionLabel(options.areaOptions.value, value)
|
||||
return options.styleOptions.value.find((item) => item.value === value)?.label || value
|
||||
}
|
||||
|
||||
const removeOptionTag = (kind: OptionTagKind) => {
|
||||
const tag = optionTags.value[kind]
|
||||
if (tag?.parentNode) {
|
||||
removeNodeWithNextSpacer(tag)
|
||||
}
|
||||
delete optionTags.value[kind]
|
||||
}
|
||||
|
||||
const removeAllOptionTags = () => {
|
||||
optionTagOrder.forEach(removeOptionTag)
|
||||
optionTags.value = {}
|
||||
}
|
||||
|
||||
const clearOptionTagValue = (kind: OptionTagKind) => {
|
||||
if (kind === 'type') {
|
||||
options.typeValue.value = ''
|
||||
} else if (kind === 'area') {
|
||||
options.areaValue.value = ''
|
||||
} else {
|
||||
options.styleValue.value = ''
|
||||
}
|
||||
|
||||
removeOptionTag(kind)
|
||||
handleEditorInput()
|
||||
}
|
||||
|
||||
const getOptionTagInsertBefore = () => {
|
||||
const editor = editorRef.value
|
||||
if (!editor) return null
|
||||
|
||||
for (const child of Array.from(editor.childNodes)) {
|
||||
if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
const childKind = (child as HTMLElement).dataset.optionTagKind as OptionTagKind
|
||||
if (childKind) continue
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const createOptionTag = (kind: OptionTagKind, label: string) => {
|
||||
const tag = document.createElement('div')
|
||||
tag.contentEditable = 'false'
|
||||
tag.className = 'editor-tag option-tag flex-center'
|
||||
tag.dataset.optionTagKind = kind
|
||||
|
||||
const icon = document.createElement('img')
|
||||
icon.className = 'option-tag-icon'
|
||||
icon.src = getOptionTagIcon(kind) as unknown as string
|
||||
|
||||
const textSpan = document.createElement('span')
|
||||
textSpan.className = 'option-tag-text'
|
||||
textSpan.innerText = label
|
||||
|
||||
const close = document.createElement('img')
|
||||
close.className = 'close-icon option-close'
|
||||
close.src = closeIcon as unknown as string
|
||||
close.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation()
|
||||
clearOptionTagValue(kind)
|
||||
})
|
||||
|
||||
tag.appendChild(icon)
|
||||
tag.appendChild(textSpan)
|
||||
tag.appendChild(close)
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
const syncOptionTag = (kind: OptionTagKind) => {
|
||||
const value = getOptionTagValue(kind)
|
||||
if (!value) {
|
||||
removeOptionTag(kind)
|
||||
handleEditorInput()
|
||||
return
|
||||
}
|
||||
|
||||
const label = getOptionTagLabel(kind)
|
||||
const existingTag = optionTags.value[kind]
|
||||
|
||||
if (existingTag?.parentNode) {
|
||||
const text = existingTag.querySelector('.option-tag-text')
|
||||
if (text) text.textContent = label
|
||||
handleEditorInput()
|
||||
return
|
||||
}
|
||||
|
||||
const editor = editorRef.value
|
||||
if (!editor) return
|
||||
|
||||
const tag = createOptionTag(kind, label)
|
||||
const referenceNode = getOptionTagInsertBefore()
|
||||
if (referenceNode) {
|
||||
editor.insertBefore(tag, referenceNode)
|
||||
} else {
|
||||
editor.appendChild(tag)
|
||||
}
|
||||
|
||||
if (tag.parentNode) {
|
||||
tag.parentNode.insertBefore(document.createTextNode('\u200B'), tag.nextSibling)
|
||||
}
|
||||
|
||||
optionTags.value[kind] = tag
|
||||
handleEditorInput()
|
||||
}
|
||||
|
||||
const syncAllOptionTags = () => {
|
||||
optionTagOrder.forEach(syncOptionTag)
|
||||
}
|
||||
|
||||
const showPlaceholder = computed(() => {
|
||||
if (!editorRef.value) return true
|
||||
if (inputValue.value || reportTags.value.length > 0 || hasOptionTags()) return false
|
||||
|
||||
const editor = editorRef.value
|
||||
const textContent = editor.textContent?.replace(/[\s\u200B\n]/g, '').trim() || ''
|
||||
const isEmptyHTML = editor.innerHTML === '' || editor.innerHTML === '<br>'
|
||||
const hasMeaningfulChildren = Array.from(editor.children).some(
|
||||
(child) => child.tagName !== 'BR' && !child.classList.contains('editor-tag')
|
||||
)
|
||||
|
||||
return textContent === '' && !hasMeaningfulChildren && isEmptyHTML
|
||||
})
|
||||
|
||||
const addReportTag = (text?: string) => {
|
||||
const tagText = text || options.t('Input.trendingReport')
|
||||
|
||||
const tag = document.createElement('div')
|
||||
tag.contentEditable = 'false'
|
||||
const imgLeft = document.createElement('img')
|
||||
const imgClose = document.createElement('img')
|
||||
const textSpan = document.createElement('span')
|
||||
imgClose.className = 'close-icon'
|
||||
if (text) {
|
||||
tag.className = 'editor-tag restore flex-center'
|
||||
imgLeft.className = 'restore-icon'
|
||||
imgLeft.src = restoreIcon as unknown as string
|
||||
imgClose.src = restoreCloseIcon as unknown as string
|
||||
imgClose.className = 'close-icon restore'
|
||||
textSpan.className = 'restore-text'
|
||||
} else {
|
||||
tag.className = 'editor-tag report-btn flex-center'
|
||||
imgLeft.className = 'light-icon'
|
||||
imgLeft.src = lightIcon as unknown as string
|
||||
imgClose.src = closeIcon as unknown as string
|
||||
}
|
||||
|
||||
textSpan.innerText = tagText
|
||||
|
||||
imgClose.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation()
|
||||
removeReportPromptText()
|
||||
tag.remove()
|
||||
const idx = reportTags.value.indexOf(tag)
|
||||
if (idx > -1) reportTags.value.splice(idx, 1)
|
||||
handleEditorInput()
|
||||
})
|
||||
|
||||
tag.appendChild(imgLeft)
|
||||
tag.appendChild(textSpan)
|
||||
tag.appendChild(imgClose)
|
||||
|
||||
const selection = window.getSelection()
|
||||
const isInEditor =
|
||||
editorRef.value && selection && editorRef.value.contains(selection.anchorNode)
|
||||
|
||||
if (isInEditor && selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0)
|
||||
range.insertNode(tag)
|
||||
focusEditor()
|
||||
} else if (editorRef.value) {
|
||||
editorRef.value.appendChild(tag)
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
reportTags.value.push(tag)
|
||||
const promptText = document.createTextNode('')
|
||||
if (tag.parentNode) {
|
||||
tag.parentNode.insertBefore(promptText, tag.nextSibling)
|
||||
const zwsp = document.createTextNode('\u200B')
|
||||
tag.parentNode.insertBefore(zwsp, promptText.nextSibling)
|
||||
const newRange = document.createRange()
|
||||
newRange.setStart(promptText, promptText.data.length)
|
||||
newRange.collapse(true)
|
||||
const currentSelection = window.getSelection()
|
||||
currentSelection?.removeAllRanges()
|
||||
currentSelection?.addRange(newRange)
|
||||
}
|
||||
reportPromptText.value = promptText
|
||||
typeReportPromptText(promptText, options.t('Input.reportPlaceholder'))
|
||||
}
|
||||
|
||||
const toggleReportTag = (clear = false) => {
|
||||
const shouldClear = clear === true
|
||||
reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null)
|
||||
|
||||
if (shouldClear) {
|
||||
removeReportPromptText()
|
||||
reportTags.value.forEach((tag) => {
|
||||
tag.remove()
|
||||
})
|
||||
reportTags.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (reportTags.value.length > 0) {
|
||||
removeReportPromptText()
|
||||
reportTags.value.forEach((tag) => {
|
||||
if (
|
||||
tag.nextSibling &&
|
||||
tag.nextSibling.nodeType === Node.TEXT_NODE &&
|
||||
tag.nextSibling.textContent === '\u200B'
|
||||
) {
|
||||
tag.nextSibling.remove()
|
||||
}
|
||||
tag.remove()
|
||||
})
|
||||
reportTags.value = []
|
||||
handleEditorInput()
|
||||
} else {
|
||||
addReportTag()
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupEditor = () => {
|
||||
if (!editorRef.value) return
|
||||
|
||||
const editor = editorRef.value
|
||||
|
||||
if (editor.textContent) {
|
||||
const cleanedText = editor.textContent.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
|
||||
if (!cleanedText.trim() && editor.children.length === 0) {
|
||||
editor.innerHTML = ''
|
||||
editor.textContent = ''
|
||||
reportTags.value = []
|
||||
removeAllOptionTags()
|
||||
stopReportTypewriter()
|
||||
reportPromptText.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const autoResizeEditor = () => {
|
||||
if (options.isAgentMode.value) return
|
||||
}
|
||||
|
||||
const handleEditorInput = () => {
|
||||
if (!editorRef.value) return
|
||||
|
||||
let text = ''
|
||||
const walker = document.createTreeWalker(editorRef.value, NodeFilter.SHOW_TEXT, null)
|
||||
|
||||
let node: Node | null
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.parentElement?.closest('.editor-tag')) continue
|
||||
text += node.textContent
|
||||
}
|
||||
|
||||
text = text.replace(/[\s\u200B]+$/, '')
|
||||
inputValue.value = text
|
||||
|
||||
const editor = editorRef.value
|
||||
const hasChildElements = editor.children.length > 0
|
||||
const hasTextContent = editor.textContent?.replace(/[\s\u200B]/g, '').trim().length > 0
|
||||
|
||||
if (!hasChildElements && !hasTextContent) {
|
||||
editor.innerHTML = ''
|
||||
reportTags.value = []
|
||||
removeAllOptionTags()
|
||||
stopReportTypewriter()
|
||||
reportPromptText.value = null
|
||||
}
|
||||
|
||||
autoResizeEditor()
|
||||
}
|
||||
|
||||
const handleEditorPaste = (e: ClipboardEvent) => {
|
||||
e.preventDefault()
|
||||
const text = e.clipboardData?.getData('text/plain') || ''
|
||||
document.execCommand('insertText', false, text)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
options.onSubmit()
|
||||
return
|
||||
}
|
||||
if (e.key !== 'Backspace') return
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount <= 0) return
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!range.collapsed) return
|
||||
|
||||
let nodeToDelete: Node | null = null
|
||||
const startContainer = range.startContainer
|
||||
const startOffset = range.startOffset
|
||||
|
||||
if (startContainer.nodeType === Node.TEXT_NODE) {
|
||||
const textNode = startContainer as Text
|
||||
if (startOffset === textNode.length) {
|
||||
nodeToDelete = textNode.nextSibling
|
||||
}
|
||||
} else if (startContainer.nodeType === Node.ELEMENT_NODE) {
|
||||
nodeToDelete = startContainer.childNodes[startOffset]
|
||||
}
|
||||
|
||||
if (
|
||||
nodeToDelete &&
|
||||
nodeToDelete.nodeType === Node.ELEMENT_NODE &&
|
||||
(nodeToDelete as Element).classList
|
||||
) {
|
||||
const element = nodeToDelete as Element
|
||||
const isEditorTag = element.classList.contains('editor-tag')
|
||||
const isReportTag = element.classList.contains('report-tag')
|
||||
const optionTagKind = (element as HTMLElement).dataset.optionTagKind as OptionTagKind
|
||||
|
||||
if (optionTagKind) {
|
||||
e.preventDefault()
|
||||
clearOptionTagValue(optionTagKind)
|
||||
nextTick(() => {
|
||||
cleanupEditor()
|
||||
handleEditorInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditorTag || isReportTag) {
|
||||
e.preventDefault()
|
||||
if (reportTags.value.includes(element as HTMLElement)) {
|
||||
removeReportPromptText()
|
||||
}
|
||||
element.remove()
|
||||
|
||||
const index = reportTags.value.indexOf(nodeToDelete as HTMLElement)
|
||||
if (index > -1) {
|
||||
reportTags.value.splice(index, 1)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
cleanupEditor()
|
||||
handleEditorInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (editorRef.value) {
|
||||
const text = editorRef.value.textContent || ''
|
||||
const cleanedText = text.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
|
||||
if (editorRef.value.innerHTML === '<br>' || !cleanedText.trim()) {
|
||||
editorRef.value.innerHTML = ''
|
||||
if (editorRef.value.children.length === 0) {
|
||||
reportTags.value = []
|
||||
reportPromptText.value = null
|
||||
}
|
||||
}
|
||||
handleEditorInput()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearEditorText = () => {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.innerHTML = ''
|
||||
}
|
||||
inputValue.value = ''
|
||||
}
|
||||
|
||||
const resetOptionTags = () => {
|
||||
optionTags.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
editorRef,
|
||||
inputValue,
|
||||
reportTags,
|
||||
showPlaceholder,
|
||||
setEditorElement,
|
||||
focusEditor,
|
||||
stopReportTypewriter,
|
||||
addReportTag,
|
||||
toggleReportTag,
|
||||
handleEditorInput,
|
||||
handleEditorPaste,
|
||||
handleKeyDown,
|
||||
autoResizeEditor,
|
||||
syncOptionTag,
|
||||
syncAllOptionTags,
|
||||
clearEditorText,
|
||||
resetOptionTags
|
||||
}
|
||||
}
|
||||
78
src/views/home/components/input/useInputImages.ts
Normal file
78
src/views/home/components/input/useInputImages.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { uploadImage } from '@/api/upload'
|
||||
import type { PreviewImage, UploadedImage } from './types'
|
||||
|
||||
export function useInputImages(focusEditor: () => void) {
|
||||
const uploadedImages = ref<UploadedImage[]>([])
|
||||
const quoteList = ref<string[]>([])
|
||||
const showPreview = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
|
||||
if (input.files) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
uploadImage(formData).then((res) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
uploadedImages.value.push({
|
||||
url: e.target?.result as string,
|
||||
name: file.name,
|
||||
path: res
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
nextTick(focusEditor)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const removeImage = (index: number, item: PreviewImage) => {
|
||||
if (typeof item === 'string') {
|
||||
const quoteIndex = quoteList.value.indexOf(item)
|
||||
if (quoteIndex > -1) {
|
||||
quoteList.value.splice(quoteIndex, 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
uploadedImages.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const previewImage = (url: string) => {
|
||||
showPreview.value = true
|
||||
previewUrl.value = url
|
||||
}
|
||||
|
||||
const handleQuote = (url: string) => {
|
||||
const hasQuoted = quoteList.value.includes(url)
|
||||
if (hasQuoted) return
|
||||
quoteList.value[0] = url
|
||||
}
|
||||
|
||||
const clearImages = () => {
|
||||
uploadedImages.value = []
|
||||
quoteList.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
uploadedImages,
|
||||
quoteList,
|
||||
showPreview,
|
||||
previewUrl,
|
||||
handleFileChange,
|
||||
removeImage,
|
||||
previewImage,
|
||||
handleQuote,
|
||||
clearImages
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user