Skip to content

Commit 0d2322b

Browse files
committed
feat(ui): 更新图像模式模板选择逻辑与组件交互
- 在storage-keys.ts中新增文本到图像和图像到图像的模板选择存储键 - 在App.vue中优化模板管理器关闭逻辑,确保图像模式下模板列表刷新 - 在ImageWorkspace.vue中引入SelectWithConfig组件,增强模板选择的灵活性与可配置性 - 在useImageWorkspace.ts中完善模板恢复逻辑,支持不同模式下的模板选择持久化 - 在ModelManager.vue中支持外部事件切换页签,提升用户体验 - 增强了模板选择的用户交互反馈,确保更好的使用体验
1 parent 9fa70ef commit 0d2322b

File tree

8 files changed

+1629
-1172
lines changed

8 files changed

+1629
-1172
lines changed

packages/core/src/constants/storage-keys.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export const TEMPLATE_SELECTION_KEYS = {
4141
export const IMAGE_MODE_KEYS = {
4242
SELECTED_TEXT_MODEL: 'app:image-mode:selected-text-model',
4343
SELECTED_IMAGE_MODEL: 'app:image-mode:selected-image-model',
44-
SELECTED_TEMPLATE: 'app:image-mode:selected-template',
44+
// 按模式分别存储模板选择
45+
SELECTED_TEMPLATE_TEXT2IMAGE: 'app:image-mode:selected-template:text2image',
46+
SELECTED_TEMPLATE_IMAGE2IMAGE: 'app:image-mode:selected-template:image2image',
4547
SELECTED_ITERATE_TEMPLATE: 'app:image-mode:selected-iterate-template',
4648
COMPARE_MODE_ENABLED: 'app:image-mode:compare-mode-enabled',
4749
} as const

packages/extension/src/App.vue

Lines changed: 1145 additions & 1121 deletions
Large diffs are not rendered by default.

packages/ui/src/components/ModelManager.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@
677677
</template>
678678

679679
<script setup lang="ts">
680-
import { ref, onMounted, watch, computed, inject, provide } from 'vue'; // Added computed, inject and provide
680+
import { ref, onMounted, onUnmounted, watch, computed, inject, provide } from 'vue'; // Added computed, inject and provide
681681
import { useI18n } from 'vue-i18n';
682682
import {
683683
NModal, NScrollbar, NSpace, NCard, NText, NH4, NTag, NButton,
@@ -717,6 +717,18 @@ const close = () => {
717717
// 活动标签(文本/图像)
718718
const activeTab = ref('text')
719719
720+
// 支持外部通过事件切换页签
721+
if (typeof window !== 'undefined') {
722+
const tabHandler = (e: Event) => {
723+
try {
724+
const tab = (e as CustomEvent).detail
725+
if (tab === 'text' || tab === 'image') activeTab.value = tab
726+
} catch {}
727+
}
728+
onMounted(() => window.addEventListener('model-manager:set-tab', tabHandler))
729+
onUnmounted(() => window.removeEventListener('model-manager:set-tab', tabHandler))
730+
}
731+
720732
721733
// 打开新增弹窗(根据活动标签)
722734
const openAddForActiveTab = () => {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<template>
2+
<NSelect
3+
v-bind="forwardedAttrs"
4+
:value="normalizedValue"
5+
:options="mappedOptions"
6+
:render-label="renderOptionLabel"
7+
:render-tag="multiple ? renderSelectedTag : undefined"
8+
@update:value="onUpdateValue"
9+
>
10+
<template #empty>
11+
<slot name="empty">
12+
<NSpace vertical align="center" style="padding: 12px 0;">
13+
<NText depth="3">{{ emptyText || t('model.select.noAvailableModels') }}</NText>
14+
<NButton
15+
v-if="shouldShowEmptyConfigCTA"
16+
type="tertiary"
17+
size="small"
18+
ghost
19+
@click="emitConfig()"
20+
>
21+
<template #icon>
22+
<span>⚙️</span>
23+
</template>
24+
{{ configText || t('model.select.configure') }}
25+
</NButton>
26+
</NSpace>
27+
</slot>
28+
</template>
29+
30+
<template #action>
31+
<slot name="action">
32+
<div v-if="shouldShowConfigAction" style="padding: 8px 12px;">
33+
<NButton quaternary size="small" @click="emitConfig()">
34+
<template #icon>
35+
<span>⚙️</span>
36+
</template>
37+
{{ configText || t('model.select.configure') }}
38+
</NButton>
39+
</div>
40+
</slot>
41+
</template>
42+
</NSelect>
43+
</template>
44+
45+
<script setup lang="ts">
46+
import { computed, h, useAttrs } from 'vue'
47+
import { useI18n } from 'vue-i18n'
48+
import { NSelect, NSpace, NButton, NText } from 'naive-ui'
49+
50+
interface Props {
51+
modelValue: any
52+
options: any[]
53+
getPrimary: (opt: any) => string
54+
getSecondary?: (opt: any) => string
55+
getValue: (opt: any) => string | number
56+
selectedTooltip?: boolean
57+
showConfigAction?: boolean
58+
showEmptyConfigCTA?: boolean
59+
configText?: string
60+
emptyText?: string
61+
multiple?: boolean
62+
}
63+
64+
const props = withDefaults(defineProps<Props>(), {
65+
modelValue: null,
66+
options: () => [],
67+
getSecondary: undefined,
68+
selectedTooltip: true,
69+
showConfigAction: false,
70+
showEmptyConfigCTA: false,
71+
configText: undefined,
72+
emptyText: undefined,
73+
multiple: false
74+
})
75+
76+
const emit = defineEmits<{
77+
'update:modelValue': [value: any]
78+
'config': [payload?: any]
79+
}>()
80+
81+
const attrs = useAttrs() as Record<string, any>
82+
const { t } = useI18n()
83+
84+
// 检测是否有 config 事件处理器 - 始终显示配置按钮确保功能可用
85+
// 动态显示配置相关功能(由父级显式开启)
86+
const shouldShowConfigAction = computed(() => !!props.showConfigAction)
87+
const shouldShowEmptyConfigCTA = computed(() => !!props.showEmptyConfigCTA)
88+
89+
// 将外部原始 options 转换为 NSelect 可识别的选项,label 为两行结构
90+
const mappedOptions = computed(() => {
91+
return (props.options || []).map((opt: any) => {
92+
const primary = props.getPrimary(opt) || ''
93+
const secondary = props.getSecondary ? (props.getSecondary(opt) || '') : ''
94+
const value = props.getValue(opt)
95+
return {
96+
label: primary,
97+
value,
98+
raw: opt,
99+
primary,
100+
secondary
101+
}
102+
})
103+
})
104+
105+
// 使用 Naive UI 官方的 render-label 自定义选项渲染
106+
const renderOptionLabel = (option: any) => {
107+
const primary = option?.primary || ''
108+
const secondary = option?.secondary || ''
109+
const title = props.selectedTooltip && secondary ? `${primary} · ${secondary}` : undefined
110+
return h('div', { class: 'swc-opt', title }, [
111+
h('div', { class: 'swc-primary' }, primary),
112+
secondary ? h('div', { class: 'swc-secondary' }, secondary) : null
113+
])
114+
}
115+
116+
// 多选 tag 渲染
117+
const renderSelectedTag = ({ option }: { option: any }) => {
118+
const title = props.selectedTooltip && option?.secondary ? `${option.primary} · ${option.secondary}` : undefined
119+
return h('span', { title }, option?.primary || '')
120+
}
121+
122+
// 透传属性,若无自定义 filter,则提供默认过滤(匹配主/副文本)
123+
const forwardedAttrs = computed(() => {
124+
const hasCustomFilter = Object.prototype.hasOwnProperty.call(attrs, 'filter')
125+
const internalFilter = (pattern: string, option: any) => {
126+
const p = (pattern || '').toLowerCase()
127+
return (
128+
(option?.primary || '').toLowerCase().includes(p) ||
129+
(option?.secondary || '').toLowerCase().includes(p)
130+
)
131+
}
132+
const { ['onUpdate:value']: _omitUpdate, multiple: attrsMultiple, class: rootClass, ['menu-props']: menuPropsKebab, menuProps, ...rest } = attrs as any
133+
134+
// 规范:通过 class & menu-props.class 注入样式作用域,避免使用 :deep
135+
const mergedRootClass = [rootClass, 'swc-select'].filter(Boolean).join(' ')
136+
const mp = (menuPropsKebab || menuProps || {}) as Record<string, any>
137+
const mergedMenuClass = [mp.class, 'swc-select-menu'].filter(Boolean).join(' ')
138+
const normalizedMenuProps = { ...mp, class: mergedMenuClass }
139+
140+
return {
141+
filterable: true,
142+
multiple: attrsMultiple ?? props.multiple,
143+
class: mergedRootClass,
144+
menuProps: normalizedMenuProps,
145+
...rest,
146+
filter: hasCustomFilter ? (attrs as any).filter : internalFilter
147+
}
148+
})
149+
150+
const normalizedValue = computed(() => props.modelValue)
151+
152+
const onUpdateValue = (val: any) => {
153+
emit('update:modelValue', val)
154+
const cb = (attrs as any)['onUpdate:value']
155+
if (typeof cb === 'function') cb(val)
156+
}
157+
158+
const emitConfig = () => emit('config')
159+
</script>
160+
161+
<style scoped>
162+
.swc-opt {
163+
display: flex;
164+
flex-direction: column;
165+
}
166+
.swc-primary {
167+
font-weight: 500;
168+
line-height: 1.35;
169+
}
170+
.swc-secondary {
171+
font-size: 12px;
172+
opacity: 0.72;
173+
line-height: 1.3;
174+
white-space: normal;
175+
}
176+
</style>
177+
178+
<style>
179+
/* 使用类作用域(通过 class & menu-props 注入),避免 :deep */
180+
.swc-select-menu .n-base-select-option__content {
181+
white-space: normal;
182+
line-height: 1.35;
183+
display: block;
184+
}
185+
.swc-select-menu .n-base-select-option {
186+
align-items: flex-start;
187+
padding-top: 6px;
188+
padding-bottom: 6px;
189+
}
190+
.swc-select-menu .swc-opt {
191+
display: flex;
192+
flex-direction: column;
193+
align-items: flex-start;
194+
width: 100%;
195+
}
196+
.swc-select-menu .swc-primary {
197+
font-weight: 600;
198+
line-height: 1.35;
199+
margin-bottom: 2px;
200+
}
201+
.swc-select-menu .swc-secondary {
202+
font-size: 12px;
203+
opacity: 0.65;
204+
line-height: 1.25;
205+
white-space: normal;
206+
word-break: break-word;
207+
}
208+
/* 选中区仅显示主行 */
209+
.swc-select .n-base-selection .swc-secondary,
210+
.swc-select .n-base-selection-label .swc-secondary {
211+
display: none;
212+
}
213+
</style>

0 commit comments

Comments
 (0)