refactor(StyleSelector): simplify component by removing agent integration and using unified prompt list

Removed separate user prompts and favorite agents groups, replaced with a single unified list from /ai/user-prompt/my-list endpoint. Removed agent-related functionality including API calls, filtering logic, and type handling since the component now only works with user prompts. Simplified the UI structure and filtering mechanism to work with the new data model.
This commit is contained in:
2026-03-15 21:47:33 +08:00
parent 1da5d283a9
commit 711e412afb
5 changed files with 33 additions and 1694 deletions

View File

@@ -1,604 +0,0 @@
<template>
<div class="prompt-selector">
<!-- 下拉菜单式选择器推荐方案 -->
<div v-if="displayMode === 'select'" class="prompt-select-container">
<a-select
v-model:value="selectedPromptId"
placeholder="选择文案风格"
style="width: 100%"
@change="handleSelectChange"
:loading="loading"
>
<a-select-option
v-for="prompt in allPrompts"
:key="prompt.id"
:value="prompt.id"
>
<div class="prompt-option">
<div class="prompt-option-left">
<span class="prompt-option-name">{{ prompt.name }}</span>
<span v-if="prompt.category" class="prompt-option-tag category">{{ prompt.category }}</span>
</div>
<span class="prompt-option-tag" :class="prompt.source === 'created' ? 'created' : 'favorite'">
{{ prompt.source === 'created' ? '自建' : '收藏' }}
</span>
</div>
</a-select-option>
</a-select>
</div>
<!-- 标签式选择器 -->
<div v-else-if="displayMode === 'tags'" class="prompt-tags-container">
<div class="prompt-tags-grid">
<div
v-for="prompt in displayPrompts"
:key="prompt.id"
class="prompt-tag"
:class="{ 'prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="prompt-tag-name">{{ prompt.name }}</span>
</div>
</div>
<div v-if="hasMore && showMoreButton" class="prompt-more-button">
<a-button size="small" type="link" @click="showAllPromptsModal = true" :disabled="allPrompts.length === 0">
更多 ({{ allPrompts.length - displayCount }})
</a-button>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading && allPrompts.length === 0" class="prompt-loading">
<a-spin size="small" />
<span class="loading-text">加载中...</span>
</div>
<!-- 更多提示词弹窗 -->
<div v-if="showAllPromptsModal" class="prompt-modal-mask" @click.self="showAllPromptsModal = false">
<div class="prompt-modal">
<div class="prompt-modal-header">
<h3 class="prompt-modal-title">选择提示词</h3>
<button class="prompt-modal-close" @click="showAllPromptsModal = false">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="prompt-modal-search">
<svg class="prompt-search-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
v-model="promptSearchKeyword"
type="text"
placeholder="搜索提示词..."
class="prompt-search-input"
/>
<button class="refresh-button" @click="refreshUserPrompts" :disabled="loading">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
</div>
<!-- 提示词列表 -->
<div class="prompt-modal-content">
<div v-if="filteredPrompts.length > 0" class="all-prompts-grid">
<div
v-for="prompt in filteredPrompts"
:key="prompt.id"
class="all-prompt-tag"
:class="{ 'all-prompt-tag-selected': selectedPromptId === prompt.id }"
@click="selectPrompt(prompt)"
>
<span class="all-prompt-tag-name">{{ prompt.name }}</span>
<span v-if="prompt.status === 1" class="all-prompt-tag-status">启用</span>
<span v-else class="all-prompt-tag-status all-prompt-tag-status-disabled">禁用</span>
</div>
</div>
<div v-else class="prompt-empty-state">
<div class="prompt-empty-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
</div>
<p class="prompt-empty-text">没有找到匹配的提示词</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { usePromptStore } from '@/stores/prompt'
import { message } from 'ant-design-vue'
import { setJSON, getJSON } from '@/utils/storage'
// Props
const props = defineProps({
// 当前选中的提示词ID
modelValue: {
type: Number,
default: null
},
// 展示模式tags标签或 select下拉选择
displayMode: {
type: String,
default: 'select'
},
// 展示数量(仅标签模式有效)
displayCount: {
type: Number,
default: 6
},
// 是否显示"更多"按钮
showMoreButton: {
type: Boolean,
default: true
},
// 本地存储键名前缀
storageKey: {
type: String,
default: 'prompt_selector'
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'change'])
// Stores - 单一数据源
const userStore = useUserStore()
const promptStore = usePromptStore()
// Refs
const showAllPromptsModal = ref(false)
const promptSearchKeyword = ref('')
const selectedPromptId = ref(props.modelValue)
// ===== 单一数据源:从 Store 获取 =====
const allPrompts = computed(() => promptStore.promptList)
const loading = computed(() => promptStore.promptListLoading)
// 计算属性:展示的部分提示词
const displayPrompts = computed(() => {
if (props.displayMode !== 'tags') return []
return allPrompts.value.slice(0, props.displayCount)
})
// 计算属性:是否有更多提示词
const hasMore = computed(() => {
return allPrompts.value.length > props.displayCount
})
// 计算属性:过滤后的全部提示词(用于"更多"弹窗)
const filteredPrompts = computed(() => {
if (!promptSearchKeyword.value.trim()) {
return allPrompts.value
}
const keyword = promptSearchKeyword.value.trim().toLowerCase()
return allPrompts.value.filter(p =>
p.name.toLowerCase().includes(keyword) ||
(p.content && p.content.toLowerCase().includes(keyword))
)
})
// 监听 selectedPromptId 变化,同步到父组件
watch(selectedPromptId, (newValue) => {
emit('update:modelValue', newValue)
saveSelectedPromptId(newValue)
})
// 监听 props.modelValue 变化,更新内部状态
watch(() => props.modelValue, (newValue) => {
selectedPromptId.value = newValue
})
// 加载用户提示词(通过 Store
async function loadUserPrompts() {
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
return
}
try {
// 使用 store 加载(自建 + 收藏的智能体)
await promptStore.loadPromptList()
// 如果有选中ID验证是否在列表中
if (selectedPromptId.value) {
const exists = allPrompts.value.find(p => p.id === selectedPromptId.value)
if (!exists) {
selectedPromptId.value = null
}
}
// 如果没有选中且有提示词,默认选中第一个
if (!selectedPromptId.value && allPrompts.value.length > 0) {
selectedPromptId.value = allPrompts.value[0].id
}
// 恢复本地存储的选择
await restoreSelectedPromptId()
} catch (error) {
console.error('加载提示词失败:', error)
}
}
// 保存选中的提示词ID到本地存储
async function saveSelectedPromptId(promptId) {
if (!promptId) return
try {
await setJSON(`${props.storageKey}_selected`, promptId)
} catch (error) {
console.error('保存提示词选择失败:', error)
}
}
// 从本地存储恢复选中的提示词ID
async function restoreSelectedPromptId() {
try {
const savedId = await getJSON(`${props.storageKey}_selected`, null)
if (savedId) {
// 检查保存的ID是否在当前列表中
const prompt = allPrompts.value.find(p => p.id === savedId)
if (prompt) {
selectedPromptId.value = savedId
}
}
} catch (error) {
console.error('恢复提示词选择失败:', error)
}
}
// 选择提示词
function selectPrompt(prompt) {
if (!prompt || !prompt.content) {
console.warn('提示词内容为空')
return
}
selectedPromptId.value = prompt.id
emit('change', prompt)
showAllPromptsModal.value = false
}
// 处理下拉选择变化
function handleSelectChange(value) {
const prompt = allPrompts.value.find(p => p.id === value)
if (prompt) {
emit('change', prompt)
}
}
// 刷新用户提示词(强制重新加载)
async function refreshUserPrompts() {
try {
await promptStore.refreshPromptList()
await restoreSelectedPromptId()
} catch (error) {
console.error('刷新提示词失败:', error)
message.error('刷新提示词失败')
}
}
// 组件挂载时加载提示词
onMounted(() => {
loadUserPrompts()
})
</script>
<style scoped>
/* 基础样式 */
.prompt-selector {
width: 100%;
}
/* 下拉选择器样式 */
.prompt-select-container {
width: 100%;
}
.prompt-option {
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt-option-name {
font-weight: 500;
}
.prompt-option-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.prompt-option-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
flex-shrink: 0;
}
.prompt-option-tag.category {
background: rgba(24, 144, 255, 0.1);
color: var(--color-primary);
}
.prompt-option-tag.created {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.prompt-option-tag.favorite {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
/* 标签模式 */
.prompt-tags-container {
margin-bottom: 12px;
}
.prompt-tags-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.prompt-tag {
display: inline-flex;
align-items: center;
padding: 6px 14px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.prompt-tag:hover {
border-color: var(--color-primary);
background: rgba(24, 144, 255, 0.08);
}
.prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.prompt-more-button {
margin-top: 8px;
text-align: right;
}
/* 加载状态 */
.prompt-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
color: var(--color-text-secondary);
}
.loading-text {
font-size: 14px;
}
/* 空状态 */
.prompt-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--color-text-secondary);
}
.prompt-empty-icon {
color: var(--color-text-secondary);
opacity: 0.5;
margin-bottom: 12px;
}
.prompt-empty-text {
font-size: 14px;
margin: 0;
}
/* 弹窗 */
.prompt-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.prompt-modal {
background: var(--color-surface);
border-radius: var(--radius-card);
width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.prompt-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
}
.prompt-modal-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.prompt-modal-close {
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
}
.prompt-modal-close:hover {
background: var(--color-bg);
color: var(--color-text);
}
.prompt-modal-search {
position: relative;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
display: flex;
gap: 12px;
}
.prompt-search-icon {
position: absolute;
left: 36px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-secondary);
pointer-events: none;
}
.prompt-search-input {
flex: 1;
padding: 10px 12px 10px 40px;
font-size: 14px;
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
}
.prompt-search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.refresh-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text);
cursor: pointer;
}
.refresh-button:hover:not(:disabled) {
background: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
.refresh-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.prompt-modal-content {
overflow-y: auto;
max-height: calc(80vh - 180px);
padding: 16px 0;
}
/* 提示词列表 */
.all-prompts-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 16px;
}
.all-prompt-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 16px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.all-prompt-tag:hover {
border-color: var(--color-primary);
background: rgba(24, 144, 255, 0.08);
}
.all-prompt-tag-selected {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.all-prompt-tag-name {
white-space: nowrap;
user-select: none;
}
.all-prompt-tag-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.all-prompt-tag-status-disabled {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
</style>

View File

@@ -8,43 +8,20 @@
:filter-option="filterOption"
@change="handleChange"
>
<!-- 用户风格分组 -->
<a-select-opt-group v-if="userPrompts.length > 0">
<template #label>
<span class="group-label">用户风格</span>
</template>
<a-select-option
v-for="item in userPrompts"
:key="`prompt-${item.id}`"
:value="`prompt-${item.id}`"
>
<div class="option-content">
<span class="option-name">{{ item.name }}</span>
<span class="option-tag style-tag">风格</span>
</div>
</a-select-option>
</a-select-opt-group>
<!-- 收藏的智能体分组 -->
<a-select-opt-group v-if="favoriteAgents.length > 0">
<template #label>
<span class="group-label">收藏的智能体</span>
</template>
<a-select-option
v-for="item in favoriteAgents"
:key="`agent-${item.id}`"
:value="`agent-${item.id}`"
>
<div class="option-content">
<img v-if="item.avatar" :src="item.avatar" class="option-avatar" />
<span class="option-name">{{ item.name }}</span>
<span class="option-tag agent-tag">{{ item.categoryName || '智能体' }}</span>
</div>
</a-select-option>
</a-select-opt-group>
<a-select-option
v-for="item in allList"
:key="item.id"
:value="item.id"
>
<div class="option-content">
<img v-if="item.icon" :src="item.icon" class="option-avatar" />
<span class="option-name">{{ item.name }}</span>
<span class="option-tag">{{ item.category || '其他' }}</span>
</div>
</a-select-option>
<!-- 空状态 -->
<template v-if="!loading && userPrompts.length === 0 && favoriteAgents.length === 0">
<template v-if="!loading && allList.length === 0">
<a-select-option disabled value="__empty__">
<span class="empty-text">暂无可选项</span>
</a-select-option>
@@ -55,7 +32,6 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { usePromptStore } from '@/stores/prompt'
import { getAgentList } from '@/api/agent'
import { getJSON, setJSON } from '@/utils/storage'
const props = defineProps({
@@ -79,32 +55,14 @@ const promptStore = usePromptStore()
const loading = ref(false)
const selectedId = ref(null)
const agentList = ref([])
// 用户风格列表
const userPrompts = computed(() => promptStore.promptList || [])
// 收藏的智能体列表
const favoriteAgents = computed(() => {
return agentList.value.filter(agent => agent.isFavorite)
})
// 全部列表(来自 /ai/user-prompt/my-list
const allList = computed(() => promptStore.promptList || [])
// 过滤选项
function filterOption(input, option) {
const key = option.value
const name = option.label || ''
// 根据 key 查找对应的项目名称
if (key.startsWith('prompt-')) {
const id = parseInt(key.replace('prompt-', ''))
const prompt = userPrompts.value.find(p => p.id === id)
return prompt?.name?.toLowerCase().includes(input.toLowerCase())
} else if (key.startsWith('agent-')) {
const id = parseInt(key.replace('agent-', ''))
const agent = agentList.value.find(a => a.id === id)
return agent?.name?.toLowerCase().includes(input.toLowerCase())
}
return false
const item = allList.value.find(p => p.id === option.value)
return item?.name?.toLowerCase().includes(input.toLowerCase())
}
// 处理选择变化
@@ -115,54 +73,14 @@ function handleChange(value) {
return
}
const [type, idStr] = value.split('-')
const id = parseInt(idStr)
let item = null
if (type === 'prompt') {
item = userPrompts.value.find(p => p.id === id)
emit('change', { id, type: 'prompt', item })
} else if (type === 'agent') {
item = agentList.value.find(a => a.id === id)
emit('change', { id, type: 'agent', item })
}
emit('update:modelValue', id)
const item = allList.value.find(p => p.id === value)
emit('change', { id: value, item })
emit('update:modelValue', value)
// 保存选中状态到本地
saveSelectedValue(value)
}
// 加载智能体列表
async function loadAgentList() {
try {
const res = await getAgentList()
if (res.code === 0 && res.data) {
agentList.value = res.data.map(item => ({
id: item.id,
agentId: item.agentId,
name: item.agentName,
description: item.description,
systemPrompt: item.systemPrompt,
avatar: item.icon,
categoryName: item.categoryName || '其他',
isFavorite: item.isFavorite || false
}))
}
} catch (error) {
console.error('加载智能体列表失败:', error)
}
}
// 加载用户风格列表
async function loadUserPrompts() {
try {
await promptStore.loadPromptList()
} catch (error) {
console.error('加载用户风格列表失败:', error)
}
}
// 保存选中状态
async function saveSelectedValue(value) {
try {
@@ -175,28 +93,13 @@ async function saveSelectedValue(value) {
// 恢复选中状态
async function restoreSelectedValue() {
try {
const savedValue = await getJSON(`${props.storageKey}_selected`, null)
if (savedValue) {
// 验证保存的值是否有效
const [type, idStr] = savedValue.split('-')
const id = parseInt(idStr)
if (type === 'prompt') {
const exists = userPrompts.value.some(p => p.id === id)
if (exists) {
selectedId.value = savedValue
emit('update:modelValue', id)
const item = userPrompts.value.find(p => p.id === id)
emit('change', { id, type: 'prompt', item })
}
} else if (type === 'agent') {
const exists = agentList.value.some(a => a.id === id)
if (exists) {
selectedId.value = savedValue
emit('update:modelValue', id)
const item = agentList.value.find(a => a.id === id)
emit('change', { id, type: 'agent', item })
}
const savedId = await getJSON(`${props.storageKey}_selected`, null)
if (savedId) {
const item = allList.value.find(p => p.id === savedId)
if (item) {
selectedId.value = savedId
emit('update:modelValue', savedId)
emit('change', { id: savedId, item })
}
}
} catch (error) {
@@ -208,7 +111,7 @@ async function restoreSelectedValue() {
async function init() {
loading.value = true
try {
await Promise.all([loadUserPrompts(), loadAgentList()])
await promptStore.loadPromptList()
await restoreSelectedValue()
} finally {
loading.value = false
@@ -233,12 +136,6 @@ defineExpose({
</script>
<style scoped lang="less">
.group-label {
font-weight: 600;
color: var(--color-text);
font-size: 13px;
}
.option-content {
display: flex;
align-items: center;
@@ -263,17 +160,9 @@ defineExpose({
font-size: 11px;
padding: 1px 6px;
border-radius: 10px;
flex-shrink: 0;
}
.style-tag {
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
}
.agent-tag {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
flex-shrink: 0;
}
.empty-text {