Files
sionrui/frontend/app/web-gold/src/components/PromptSelector.vue
sion123 e050a960c5 feat: remove redundant error notifications in prompt loading
Removed duplicate message.error calls in both PromptSelector.vue and Forecast.vue components after logging errors to console. The error messages were being displayed twice to users, creating a redundant UX. Now errors are only logged to console for debugging while maintaining clean user interface.
2026-01-18 18:38:21 +08:00

576 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
<span class="prompt-option-name">{{ prompt.name }}</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-else-if="!loading && allPrompts.length === 0" 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 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' // 默认为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
})
// 加载用户提示词
async function loadUserPrompts() {
// 检查用户是否登录
if (!userStore.userId) {
console.warn('用户未登录,无法加载提示词')
return
}
try {
// 使用 store 加载数据
const prompts = await promptStore.loadPromptList(userStore.userId)
// 如果有选中ID但当前选中的提示词不在列表中清空选择
if (selectedPromptId.value && !prompts.find(p => p.id === selectedPromptId.value)) {
selectedPromptId.value = null
}
// 如果没有选中ID且有提示词默认选中第一个
else if (!selectedPromptId.value && prompts.length > 0) {
selectedPromptId.value = prompts[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(userStore.userId)
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-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>