deotalandAi/apps/FrontendDesigner/src/views/admin/AdminPromptManagement/AdminPromptManagement.vue

579 lines
14 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-management">
<!-- 页面头部 -->
<div class="page-header">
<h2>{{ t('admin.promptManagement.title') }}</h2>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
{{ t('admin.promptManagement.addPrompt') }}
</el-button>
</div>
<!-- 左右分栏容器 -->
<div class="prompt-container">
<!-- 左侧未生效提示词区域 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.inactivePrompts') }}</h3>
<span class="count">{{ inactivePrompts.length }}</span>
</div>
<div class="prompt-list">
<el-skeleton v-if="loading" :rows="6" animated />
<PromptCard
v-else
v-for="prompt in inactivePrompts"
:key="prompt.id"
:prompt="prompt"
@edit="showEditDialog"
@delete="deletePrompt"
@click="showDetail"
@drag-start="handleLeftDragStart"
/>
</div>
</div>
<!-- 右侧:生效提示词区域 -->
<div class="prompt-section">
<div class="section-header">
<h3>{{ t('admin.promptManagement.activePrompts') }}</h3>
<span class="count">{{ activePrompts.length }}</span>
</div>
<div
class="prompt-list active-list"
@drop="handleDrop"
@dragover="handleDragOver"
>
<el-skeleton v-if="loading" :rows="6" animated />
<draggable
v-else
v-model="sortedActivePrompts"
item-key="id"
@start="handleDragStart"
@update="handleSortUpdate"
:animation="200"
:ghost-class="'ghost-card'"
:chosen-class="'chosen-card'"
:drag-class="'dragging-card'"
:axis="'y'"
>
<template #item="{ element: prompt }">
<PromptCardHorizontal
:prompt="prompt"
:isActive="true"
@edit="showEditDialog"
@delete="removeFromActive"
@click="showDetail"
/>
</template>
</draggable>
</div>
</div>
</div>
<!-- 添加/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEditing ? t('admin.promptManagement.editPrompt') : t('admin.promptManagement.addPrompt')"
width="600px"
>
<el-form :model="formData" label-width="80px">
<el-form-item :label="t('admin.promptManagement.type')" required>
<el-select v-model="formData.type" :placeholder="t('admin.promptManagement.selectType')">
<el-option :label="t('admin.promptManagement.animal')" value="animal" />
<el-option :label="t('admin.promptManagement.person')" value="person" />
<el-option :label="t('admin.promptManagement.general')" value="general" />
</el-select>
</el-form-item>
<el-form-item :label="t('admin.promptManagement.title')" required>
<el-input v-model="formData.title" :placeholder="t('admin.promptManagement.enterTitle')" />
</el-form-item>
<el-form-item :label="t('admin.promptManagement.content')" required>
<el-input
v-model="formData.content"
type="textarea"
:rows="4"
:placeholder="t('admin.promptManagement.enterContent')"
/>
</el-form-item>
<el-form-item :label="t('admin.promptManagement.referenceImage')">
<el-upload
v-model:file-list="fileList"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="1"
:on-change="handleImageChange"
:on-remove="handleImageRemove"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">
{{ t('admin.promptManagement.imageTip') }}
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="savePrompt">{{ t('common.save') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 提示词详情弹窗 -->
<el-dialog
v-model="detailVisible"
:title="t('admin.promptManagement.promptDetail')"
width="600px"
>
<div v-if="selectedPrompt" class="prompt-detail">
<div class="detail-item">
<label>{{ t('admin.promptManagement.type') }}:</label>
<span>{{ getTypeLabel(selectedPrompt.type) }}</span>
</div>
<div class="detail-item">
<label>{{ t('admin.promptManagement.title') }}:</label>
<span>{{ selectedPrompt.title }}</span>
</div>
<div class="detail-item">
<label>{{ t('admin.promptManagement.content') }}:</label>
<p>{{ selectedPrompt.content }}</p>
</div>
<div class="detail-item" v-if="selectedPrompt.imageUrls && selectedPrompt.imageUrls.length > 0">
<label>{{ t('admin.promptManagement.referenceImage') }}:</label>
<img :src="selectedPrompt.imageUrls[0]" alt="参考图" class="reference-image" />
</div>
</div>
<el-skeleton v-else :rows="4" animated />
<template #footer>
<span class="dialog-footer">
<el-button @click="detailVisible = false">{{ t('common.close') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { Plus } from '@element-plus/icons-vue'
import PromptCard from '@/components/admin/PromptCard.vue'
import PromptCardHorizontal from '@/components/admin/PromptCardHorizontal.vue'
import draggable from 'vuedraggable'
import { AdminPromptManagement } from './index.js'
const { t } = useI18n()
// 创建API实例
const promptApi = new AdminPromptManagement()
// 响应式数据
const dialogVisible = ref(false)
const detailVisible = ref(false)
const isEditing = ref(false)
const selectedPromptId = ref(null)
const selectedPrompt = ref(null)
const draggedPrompt = ref(null)
const fileList = ref([])
const prompts = ref([])
const loading = ref(false)
// 表单数据
const formData = ref({
title: '',
content: '',
type: '',
imageUrls: []
})
// 计算属性:未生效提示词
const inactivePrompts = computed(() => {
return prompts.value.filter(prompt => prompt.isActive === 0)
})
// 计算属性:生效提示词(未排序)
const activePrompts = computed(() => {
return prompts.value.filter(prompt => prompt.isActive === 1)
})
// 计算属性:排序后的生效提示词(用于拖拽排序)
const sortedActivePrompts = computed({
get: () => {
return prompts.value
.filter(prompt => prompt.isActive === 1)
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
},
set: (newValue) => {
// 当拖拽排序发生变化时更新所有生效提示词的sortOrder
newValue.forEach((prompt, index) => {
const originalPrompt = prompts.value.find(p => p.id === prompt.id)
if (originalPrompt) {
originalPrompt.sortOrder = index
}
})
}
})
// 获取提示词列表
const fetchPrompts = async () => {
try {
loading.value = true
const response = await promptApi.getPromptList({
pageSize: 99, // 不分页,获取所有数据
pageNum: 1
})
if (response.success && response.data) {
prompts.value = response.data.rows || []
}
} catch (error) {
console.error('获取提示词列表失败:', error)
} finally {
loading.value = false
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchPrompts()
})
// 显示添加弹窗
const showAddDialog = () => {
isEditing.value = false
selectedPromptId.value = null
formData.value = {
title: '',
content: '',
type: '',
imageUrls: []
}
fileList.value = []
dialogVisible.value = true
}
// 显示编辑弹窗
const showEditDialog = (prompt) => {
isEditing.value = true
selectedPromptId.value = prompt.id
formData.value = {
id: prompt.id,
title: prompt.title,
content: prompt.content,
type: prompt.type,
imageUrls: prompt.imageUrls || []
}
fileList.value = (prompt.imageUrls && prompt.imageUrls.length > 0) ? [{ url: prompt.imageUrls[0] }] : []
dialogVisible.value = true
}
// 保存提示词
const savePrompt = async () => {
try {
loading.value = true
if (isEditing.value) {
// 编辑现有提示词
await promptApi.updatePrompt(formData.value)
} else {
// 添加新提示词
await promptApi.createPrompt(formData.value)
}
dialogVisible.value = false
// 重新获取数据
await fetchPrompts()
} catch (error) {
console.error('保存提示词失败:', error)
} finally {
loading.value = false
}
}
// 删除提示词
const deletePrompt = async (promptId) => {
try {
loading.value = true
await promptApi.deletePrompt(promptId)
// 重新获取数据
await fetchPrompts()
} catch (error) {
console.error('删除提示词失败:', error)
} finally {
loading.value = false
}
}
// 从生效区域移除提示词
const removeFromActive = async (promptId) => {
try {
loading.value = true
await promptApi.deactivatePrompt(promptId)
// 重新获取数据
await fetchPrompts()
} catch (error) {
console.error('取消激活提示词失败:', error)
} finally {
loading.value = false
}
}
// 处理图片上传变化
const handleImageChange = async (file) => {
if (file.raw) {
try {
loading.value = true
// 调用上传图片API
const response = await promptApi.uploadFileCom(file.raw)
// 设置返回的图片URL
formData.value.imageUrls = [response]
} catch (error) {
console.error('上传图片失败:', error)
} finally {
loading.value = false
}
}
}
// 处理图片删除
const handleImageRemove = () => {
// 清空formData中的imageUrls数组
formData.value.imageUrls = []
}
// 处理左侧卡片拖拽开始
const handleLeftDragStart = (prompt) => {
draggedPrompt.value = prompt
}
// 处理右侧卡片拖拽开始
const handleDragStart = (evt) => {
draggedPrompt.value = evt.item.__vnode.props.prompt
}
// 处理排序更新
const handleSortUpdate = async (evt) => {
// 准备批量更新排序的数据
const sortData = {
items: sortedActivePrompts.value.map((prompt, index) => ({
id: prompt.id,
sortOrder: index
}))
}
try {
loading.value = true
await promptApi.batchUpdateSort(sortData)
// 重新获取数据
await fetchPrompts()
} catch (error) {
console.error('更新排序失败:', error)
} finally {
loading.value = false
}
}
// 处理拖拽结束(从左侧到右侧)
const handleDrop = async (event) => {
event.preventDefault()
if (draggedPrompt.value && draggedPrompt.value.isActive === 0) {
try {
loading.value = true
// 调用激活API
await promptApi.activatePrompt(draggedPrompt.value.id)
// 重新获取数据
await fetchPrompts()
} catch (error) {
console.error('激活提示词失败:', error)
} finally {
loading.value = false
draggedPrompt.value = null
}
}
}
// 处理拖拽悬停
const handleDragOver = (event) => {
event.preventDefault()
}
// 查看提示词详情
const showDetail = (prompt) => {
selectedPrompt.value = prompt
detailVisible.value = true
}
// 获取类型标签
const getTypeLabel = (type) => {
const typeMap = {
animal: t('admin.promptManagement.animal'),
person: t('admin.promptManagement.person'),
general: t('admin.promptManagement.general')
}
return typeMap[type] || type
}
</script>
<style scoped>
.prompt-management {
width: 100%;
height: 100%;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
/* 左右分栏容器 */
.prompt-container {
display: flex;
gap: 24px;
height: calc(100% - 120px);
}
/* 提示词区域 */
.prompt-section {
flex: 1;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 区域头部 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
}
.section-header h3 {
font-size: 18px;
font-weight: 600;
color: #374151;
margin: 0;
}
.count {
background: #6b7280;
color: white;
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
/* 左侧区域 */
.prompt-section:first-child {
flex: 2;
}
/* 右侧区域 */
.prompt-section:last-child {
flex: 1;
}
/* 提示词列表 */
.prompt-list {
flex: 1;
padding: 16px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
align-content: start;
}
/* 生效提示词列表 */
.active-list {
background: #f0fdf4;
min-height: 200px;
grid-template-columns: 1fr;
gap: 12px;
}
/* 拖拽相关样式 */
.ghost-card {
opacity: 0.5;
background: #e0f2fe;
border: 2px dashed #3b82f6;
transition: all 0.2s ease;
}
.chosen-card {
box-shadow: 0 0 0 2px #3b82f6;
transform: scale(1.02);
transition: all 0.2s ease;
}
.dragging-card {
opacity: 0.8;
transform: rotate(3deg);
transition: all 0.2s ease;
z-index: 1000;
}
/* 提示词详情 */
.prompt-detail {
padding: 16px 0;
}
.detail-item {
margin-bottom: 16px;
}
.detail-item label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.detail-item span, .detail-item p {
color: #6b7280;
line-height: 1.6;
}
.reference-image {
max-width: 100%;
border-radius: 8px;
margin-top: 8px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.prompt-container {
flex-direction: column;
height: auto;
}
.prompt-list {
grid-template-columns: 1fr;
}
}
</style>