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.
576 lines
15 KiB
Vue
576 lines
15 KiB
Vue
<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> |