添加新组件

This commit is contained in:
王佑琳 2026-03-20 18:57:39 +08:00
parent 72a6e436b2
commit ddb1c367b5
9 changed files with 416 additions and 196 deletions

1
components.d.ts vendored
View File

@ -26,6 +26,7 @@ declare module 'vue' {
ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default']
Img: typeof import('./src/components/Img/index.vue')['default']
Model: typeof import('./src/components/dialogBox/model/index.vue')['default']
Popover: typeof import('./src/components/Popover/index.vue')['default']
Proportion: typeof import('./src/components/dialogBox/proportion/index.vue')['default']
Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']

View File

@ -0,0 +1,213 @@
<template>
<div class="custom-popover" ref="popoverRef">
<div class="popover-trigger" ref="triggerRef" @click.stop="togglePopover">
<slot name="reference" />
</div>
<Teleport to="body">
<div
v-if="visible"
ref="contentRef"
class="popover-content"
:class="[placement]"
:style="contentStyle"
>
<slot />
</div>
</Teleport>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
placement: {
type: String,
default: 'top'
},
width: {
type: [String, Number],
default: 'auto'
},
trigger: {
type: String,
default: 'click'
}
})
const emit = defineEmits(['update:modelValue'])
const popoverRef = ref(null)
const triggerRef = ref(null)
const contentRef = ref(null)
const visible = ref(props.modelValue)
const position = ref({ top: 0, left: 0 })
const popoverId = ref(Math.random().toString(36).substr(2, 9))
if (!window.__currentOpenPopoverId__) {
window.__currentOpenPopoverId__ = null
}
const contentStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
...position.value
}))
const togglePopover = async () => {
if (visible.value) {
visible.value = false
window.__currentOpenPopoverId__ = null
} else {
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
}
if (window.__currentOpenSelectId__) {
window.dispatchEvent(new CustomEvent('close-other-selects', { detail: { excludeId: null } }))
}
visible.value = true
window.__currentOpenPopoverId__ = popoverId.value
await nextTick()
updatePosition()
}
emit('update:modelValue', visible.value)
}
const updatePosition = () => {
if (!triggerRef.value || !contentRef.value) return
const triggerRect = triggerRef.value.getBoundingClientRect()
const contentRect = contentRef.value.getBoundingClientRect()
const gap = 25
let top = 0
let left = 0
switch (props.placement) {
case 'top':
top = triggerRect.top - contentRect.height - gap
left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
break
case 'bottom':
top = triggerRect.bottom + gap
left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
break
case 'left':
top = triggerRect.top + (triggerRect.height - contentRect.height) / 2
left = triggerRect.left - contentRect.width - gap
break
case 'right':
top = triggerRect.top + (triggerRect.height - contentRect.height) / 2
left = triggerRect.right + gap
break
}
position.value = {
top: `${top}px`,
left: `${left}px`
}
}
const handleClickOutside = (e) => {
if (!visible.value) return
const triggerEl = popoverRef.value
const contentEl = contentRef.value
if (
triggerEl &&
!triggerEl.contains(e.target) &&
contentEl &&
!contentEl.contains(e.target)
) {
visible.value = false
window.__currentOpenPopoverId__ = null
emit('update:modelValue', false)
}
}
const handleCloseOtherPopovers = (e) => {
if (e.detail.excludeId !== popoverId.value) {
visible.value = false
}
}
const handleCloseOtherSelects = () => {
visible.value = false
window.__currentOpenPopoverId__ = null
}
watch(() => props.modelValue, async (val) => {
visible.value = val
if (val) {
await nextTick()
updatePosition()
}
})
onMounted(() => {
document.addEventListener('click', handleClickOutside)
window.addEventListener('resize', updatePosition)
window.addEventListener('scroll', updatePosition, true)
window.addEventListener('close-other-popovers', handleCloseOtherPopovers)
window.addEventListener('close-other-selects', handleCloseOtherSelects)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', updatePosition)
window.removeEventListener('scroll', updatePosition, true)
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
})
</script>
<style scoped>
.custom-popover {
display: inline-block;
}
.popover-trigger {
display: inline-block;
}
.popover-content {
position: fixed;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
border: 1px solid #e8e8e8;
z-index: 2000;
animation: popoverFadeIn 0.2s ease;
}
.popover-content.top {
transform-origin: bottom center;
}
.popover-content.bottom {
transform-origin: top center;
}
.popover-content.left {
transform-origin: right center;
}
.popover-content.right {
transform-origin: left center;
}
@keyframes popoverFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>

View File

@ -9,7 +9,6 @@
>
<slot name="prefix" />
<span class="select-text">{{ selectedOption || placeholder }}</span>
<!-- <div v-if="props.isArrow" class="arrow-icon" :class="{ rotate: isOpen }"><i-ep-ArrowDown /></div> -->
</div>
<div
@ -19,20 +18,45 @@
:class="position"
>
<slot name="header" />
<div
v-for="(option, index) in options"
:key="option.value || index"
class="dropdown-item"
:class="{ selected: option.value === selectedValue }"
@click.stop="selectOption(option)"
>
<slot name="option" :option="option" :selected="option.value === selectedValue">
<div class="option-content">
<span class="option-label">{{ option.label }}</span>
<span v-if="option.value === selectedValue" class="option-check"></span>
<template v-if="groupedOptions.length > 0">
<div
v-for="(group, groupIndex) in groupedOptions"
:key="groupIndex"
class="option-group"
>
<div v-if="group.label" class="group-title">{{ group.label }}</div>
<div
v-for="(option, index) in group.options"
:key="option.value || index"
class="dropdown-item"
:class="{ selected: option.value === selectedValue }"
@click.stop="selectOption(option)"
>
<slot name="option" :option="option" :selected="option.value === selectedValue">
<div class="option-content">
<span class="option-label">{{ option.label }}</span>
<span v-if="option.value === selectedValue" class="option-check"></span>
</div>
</slot>
</div>
</slot>
</div>
</div>
</template>
<template v-else>
<div
v-for="(option, index) in options"
:key="option.value || index"
class="dropdown-item"
:class="{ selected: option.value === selectedValue }"
@click.stop="selectOption(option)"
>
<slot name="option" :option="option" :selected="option.value === selectedValue">
<div class="option-content">
<span class="option-label">{{ option.label }}</span>
<span v-if="option.value === selectedValue" class="option-check"></span>
</div>
</slot>
</div>
</template>
</div>
</div>
</template>
@ -53,6 +77,10 @@ const props = defineProps({
type: Array,
default: () => []
},
groupedOptions: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请选择'
@ -96,7 +124,13 @@ const headerWidth = computed(() => {
})
const selectedOption = computed(() => {
const option = props.options.find((opt) => opt.value === selectedValue.value)
let option = props.options.find((opt) => opt.value === selectedValue.value)
if (!option && props.groupedOptions.length > 0) {
for (const group of props.groupedOptions) {
option = group.options.find((opt) => opt.value === selectedValue.value)
if (option) break
}
}
return option ? option.label : ''
})
@ -108,6 +142,9 @@ const toggleDropdown = async () => {
if (window.__currentOpenSelectId__ && window.__currentOpenSelectId__ !== selectId.value) {
window.dispatchEvent(new CustomEvent('close-other-selects', { detail: { excludeId: selectId.value } }))
}
if (window.__currentOpenPopoverId__) {
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: null } }))
}
isOpen.value = true
window.__currentOpenSelectId__ = selectId.value
await nextTick()
@ -141,12 +178,19 @@ const handleCloseOtherSelects = (e) => {
}
}
const handleCloseOtherPopovers = () => {
isOpen.value = false
window.__currentOpenSelectId__ = null
}
document.addEventListener('click', handleClickOutside)
window.addEventListener('close-other-selects', handleCloseOtherSelects)
window.addEventListener('close-other-popovers', handleCloseOtherPopovers)
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
})
</script>
@ -317,4 +361,20 @@ onBeforeUnmount(() => {
transform: scale(1);
}
}
.option-group {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.group-title {
font-size: 12px;
font-weight: 600;
color: #666;
padding: 5px 20px 8px;
margin-bottom: 4px;
}
</style>

View File

@ -14,7 +14,7 @@
</div>
</div>
<div v-if="localPreviewList.length < limit" class="upload-trigger" @click="triggerUpload">
<i-ep-plus />
<i-ep-plus color="#333333" />
<span>参考内容</span>
</div>
</div>
@ -219,7 +219,8 @@ defineExpose({
width: 56px;
height: 56px;
// border: 2px dashed #d1d5db;
background-color: #F8F9FA;
background: rgba(0, 15, 51, 0.04);
backdrop-filter: blur(5px);
border-radius: 8px;
display: flex;
align-items: center;
@ -231,7 +232,7 @@ defineExpose({
transition: all 0.3s ease;
span {
color: #999;
color: #333;
font-family: "Microsoft YaHei";
font-size: 12px;
font-style: normal;

View File

@ -1,7 +1,8 @@
<template>
<Transition name="slide-up">
<div class="input-container" :class="{ generate : !props.isGenerate }" @click="handleContainerClick">
<div v-if="!props.isGenerate" class="title">AI绘画2026</div>
<div v-if="!props.isGenerate && props.type === 'painting'" class="title">AI绘画2026</div>
<div v-if="!props.isGenerate && props.type === 'video'" class="title">AI视频2026</div>
<div class="sender-top">
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-text" @click.stop="handleScrollToBottom">回到底部<img src="@/assets/dialog/ArrowDown.svg"></div>
@ -15,7 +16,13 @@
<Sender :key="useDisplay.Sender_variant" v-model="prompt" :variant="useDisplay.Sender_variant" :placeholder="promptPlaceholder" :submit-btn-disabled="isgerenate.value" :auto-size="autoSizeConfig">
<template #prefix>
<div v-show="useDisplay.Sender_variant !== 'default'" class="prefix-self-wrap">
<div v-show="useDisplay.Sender_variant !== 'default' && props.type === 'painting'" class="prefix-self-wrap">
<Model v-model="model" v-model:typeValue="type" />
<Proportion v-model="proportion" v-model:resolution="resolution" />
<Quantity v-model="quantity" />
</div>
<div v-show="useDisplay.Sender_variant !== 'default' && props.type === 'video'" class="prefix-self-wrap">
<Model v-model="model" v-model:typeValue="type" />
<Proportion v-model="proportion" v-model:resolution="resolution" />
<Quantity v-model="quantity" />
@ -58,6 +65,10 @@ const props = defineProps({
generate: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'painting'
}
})
const router = useRouter()
@ -249,6 +260,10 @@ watch(() => type.value, (newValue) => {
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1);
}
:deep(.el-popover.el-popper){
border-radius: 20px;
}
//
.select{
background: #ffffff;

View File

@ -1,55 +1,19 @@
<template>
<el-popover trigger="click" placement="top" :width="180">
<div class="select">
<div class="model-group">
<div class="group-title">生成模型</div>
<div
v-for="item in generateModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
<div class="model-group">
<div class="group-title">编辑模型</div>
<div
v-for="item in editModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
<div class="model-group">
<div class="group-title">视觉理解模型</div>
<div
v-for="item in visionModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<template #reference>
<div class="choice-btn">
<img src="@/assets/dialog/model.svg" alt="" style="width: 16px;">
<span>{{ getModelLabel(model) }}</span>
</div>
<Select
v-model="model"
:grouped-options="modelGroups"
class="model-select"
position="top"
>
<template #prefix>
<img src="@/assets/dialog/model.svg" alt="" style="width: 16px;">
</template>
</el-popover>
</Select>
</template>
<script setup>
import Select from '@/components/Select/index.vue'
const props = defineProps({
modelValue: {
type: String,
@ -90,9 +54,20 @@ const visionModels = [
{ value: 'Qwen3.5plus', label: 'Qwen3.5plus' }
]
const selectModel = (value) => {
model.value = value
}
const modelGroups = [
{
label: '生成模型',
options: generateModels
},
{
label: '编辑模型',
options: editModels
},
{
label: '视觉理解模型',
options: visionModels
}
]
const getModelType = (value) => {
if (generateModels.find(m => m.value === value)) {
@ -106,74 +81,39 @@ const getModelType = (value) => {
}
return 'text'
}
const getModelLabel = (value) => {
const allModels = [...generateModels, ...editModels, ...visionModels]
const model = allModels.find(item => item.value === value)
return model ? model.label : value
}
</script>
<style lang="less" scoped>
.choice-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.choice-btn:hover{
background: #E5E7EB;
}
.model-select {
:deep(.select-header) {
height: 40px;
padding: 0 15px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
.select{
padding: 10px;
max-height: 510px;
overflow-y: auto;
}
&:hover {
background: #E5E7EB;
}
}
.model-group{
margin-bottom: 15px;
:deep(.select-text) {
font-size: 14px;
}
&:last-child{
margin-bottom: 0;
}
}
:deep(.dropdown-menu) {
max-height: 510px;
overflow-y: auto;
}
.group-title{
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 8px;
padding-left: 5px;
}
:deep(.dropdown-item) {
min-width: 120px;
.model-item{
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-bottom: 4px;
transition: all 0.2s ease;
&:last-child{
margin-bottom: 0;
}
&:hover{
background: #f0f0f0;
}
&.active{
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
&.active {
background: rgba(0, 15, 51, 0.10);
color: #000F33;
font-weight: 600;
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<el-popover trigger="click" placement="top" :width="400">
<Popover placement="top" :width="400">
<div class="proportion-container">
<div class="section">
<h3>选择比例</h3>
@ -53,11 +53,12 @@
<span>{{ proportion }}</span>
</div>
</template>
</el-popover>
</Popover>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import Popover from '@/components/Popover/index.vue'
const props = defineProps({
modelValue: {

View File

@ -1,19 +1,18 @@
<template>
<el-popover trigger="click" placement="top" :width="330">
<div class="quantity-container">
<div v-for="item in 4" :key="item" class="quantity-item" :class="{'selected': item == quantity}" @click="selectQuantity(item)">{{ item }}</div>
</div>
<template #reference>
<div class="choice-btn">
<img src="@/assets/dialog/quantity.svg" alt="" style="width: 16px;">
<span>{{ quantity }} </span>
</div>
<Select
v-model="quantity"
:options="quantityOptions"
class="quantity-select"
position="top"
>
<template #prefix>
<img src="@/assets/dialog/quantity.svg" alt="" style="width: 16px;">
</template>
</el-popover>
</Select>
</template>
<script setup>
import { computed } from 'vue'
import Select from '@/components/Select/index.vue'
const props = defineProps({
modelValue: {
@ -29,50 +28,39 @@ const quantity = computed({
set: (value) => emit('update:modelValue', value)
})
const selectQuantity = (value) => {
quantity.value = value
}
const quantityOptions = [
{ value: 1, label: '1 张' },
{ value: 2, label: '2 张' },
{ value: 3, label: '3 张' },
{ value: 4, label: '4 张' }
]
</script>
<style lang="less" scoped>
.choice-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.choice-btn:hover{
background: #E5E7EB;
}
.quantity-container{
display: flex;
padding: 5px;
align-items: center;
align-self: stretch;
border-radius: 10px;
background: #F8F9FA;
}
.quantity-item{
display: flex;
width: 80px;
height: 32px;
padding: 0 10px;
justify-content: center;
align-items: center;
border-radius: 5px;
cursor: pointer;
}
.quantity-item.selected{
background: #fff;
}
.quantity-item:hover{
background: #E5E7EB;
.quantity-select {
:deep(.select-header) {
height: 40px;
padding: 0 15px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
&:hover {
background: #E5E7EB;
}
}
:deep(.select-text) {
font-size: 14px;
}
:deep(.dropdown-menu) {
min-width: 80px;
}
:deep(.dropdown-item) {
min-width: 80px;
justify-content: center;
}
}
</style>

View File

@ -7,13 +7,14 @@ const route = useRoute()
const shouldShowDisplay = route.path === '/home'
const loading = route.query.loading ? false : (route.path === '/home')
const Generate = route.query.Generate || false
const type = route.query.type || 'painting'
</script>
<template>
<div class="app-container">
<dialogBox :is-generate="shouldShowDisplay" :generate="Generate" />
<dialogBox :is-generate="shouldShowDisplay" :type="type" :generate="Generate" />
<display :if="shouldShowDisplay" :loading="loading" />
<display :if="shouldShowDisplay" :type="type" :loading="loading" />
</div>
</template>