580 lines
14 KiB
Vue
580 lines
14 KiB
Vue
<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-option :label="t('admin.promptManagement.O1')" value="O1" />
|
||
</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>
|