1.0.0初始化源代码
This commit is contained in:
22
frontend/src/App.vue
Normal file
22
frontend/src/App.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
56
frontend/src/api/index.js
Normal file
56
frontend/src/api/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import request from './request'
|
||||
|
||||
// 仪表盘 API
|
||||
export const dashboardApi = {
|
||||
getStats: () => request.get('/dashboard'),
|
||||
getTokenStats: () => request.get('/dashboard/token/stats'),
|
||||
getDailyUsage: (days = 30) => request.get(`/dashboard/token/daily?days=${days}`),
|
||||
getMonthlyUsage: (months = 12) => request.get(`/dashboard/token/monthly?months=${months}`)
|
||||
}
|
||||
|
||||
// 虚拟用户 API
|
||||
export const virtualUserApi = {
|
||||
getList: (params) => request.get('/virtual-users', { params }),
|
||||
getById: (id) => request.get(`/virtual-users/${id}`),
|
||||
create: (data) => request.post('/virtual-users', data),
|
||||
generate: (data) => request.post('/virtual-users/generate', data),
|
||||
update: (id, data) => request.put(`/virtual-users/${id}`, data),
|
||||
delete: (id) => request.delete(`/virtual-users/${id}`),
|
||||
import: (file, generatePersona = true) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post('/virtual-users/import', formData, {
|
||||
params: { generate_persona: generatePersona }
|
||||
})
|
||||
},
|
||||
getStats: (id) => request.get(`/virtual-users/${id}/stats`)
|
||||
}
|
||||
|
||||
// 互动记录 API
|
||||
export const interactionApi = {
|
||||
getList: (params) => request.get('/interactions', { params }),
|
||||
execute: (data) => request.post('/interactions/execute', data),
|
||||
retry: (id) => request.post(`/interactions/retry/${id}`)
|
||||
}
|
||||
|
||||
// AI 模型 API
|
||||
export const aiModelApi = {
|
||||
getList: () => request.get('/ai-models'),
|
||||
getById: (id) => request.get(`/ai-models/${id}`),
|
||||
create: (data) => request.post('/ai-models', data),
|
||||
update: (id, data) => request.put(`/ai-models/${id}`, data),
|
||||
delete: (id) => request.delete(`/ai-models/${id}`),
|
||||
test: (data) => request.post('/ai-models/test', data)
|
||||
}
|
||||
|
||||
// 系统配置 API
|
||||
export const systemApi = {
|
||||
getSchedule: () => request.get('/system/schedule'),
|
||||
getLimits: () => request.get('/system/limits'),
|
||||
getProbabilities: () => request.get('/system/probabilities'),
|
||||
updateSchedule: (data) => request.put('/system/schedule', data),
|
||||
updateLimits: (data) => request.put('/system/limits', data),
|
||||
startScheduler: () => request.post('/system/scheduler/start'),
|
||||
stopScheduler: () => request.post('/system/scheduler/stop'),
|
||||
getSchedulerStatus: () => request.get('/system/scheduler/status')
|
||||
}
|
||||
60
frontend/src/api/request.js
Normal file
60
frontend/src/api/request.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
// TODO: 添加 token
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
let message = '请求失败'
|
||||
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
message = error.response.data.detail || '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = '未授权,请登录'
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问'
|
||||
break
|
||||
case 404:
|
||||
message = '请求地址出错'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
default:
|
||||
message = error.response.data.detail || message
|
||||
}
|
||||
} else if (error.message.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.message.includes('Network')) {
|
||||
message = '网络连接失败'
|
||||
}
|
||||
|
||||
ElMessage.error(message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
25
frontend/src/main.js
Normal file
25
frontend/src/main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册 Element Plus 图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
48
frontend/src/router/index.js
Normal file
48
frontend/src/router/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '控制台' }
|
||||
},
|
||||
{
|
||||
path: '/virtual-users',
|
||||
name: 'VirtualUsers',
|
||||
component: () => import('@/views/VirtualUsers.vue'),
|
||||
meta: { title: '虚拟用户管理' }
|
||||
},
|
||||
{
|
||||
path: '/interactions',
|
||||
name: 'Interactions',
|
||||
component: () => import('@/views/Interactions.vue'),
|
||||
meta: { title: '互动记录' }
|
||||
},
|
||||
{
|
||||
path: '/ai-models',
|
||||
name: 'AIModels',
|
||||
component: () => import('@/views/AIModels.vue'),
|
||||
meta: { title: 'AI 模型配置' }
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/Settings.vue'),
|
||||
meta: { title: '系统设置' }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 会会虚拟用户 AI 互动系统`
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
275
frontend/src/views/AIModels.vue
Normal file
275
frontend/src/views/AIModels.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="ai-models">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AI 模型配置</span>
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加模型
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="modelList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="display_name" label="名称" width="150" />
|
||||
<el-table-column prop="model_name" label="模型" width="150" />
|
||||
<el-table-column prop="provider" label="提供商" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ getProviderLabel(row.provider) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="temperature" label="温度" width="100" />
|
||||
<el-table-column prop="max_tokens" label="最大 Token" width="100" />
|
||||
<el-table-column prop="is_default" label="默认" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.is_default ? 'success' : 'info'">
|
||||
{{ row.is_default ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_active" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.is_active ? 'success' : 'info'">
|
||||
{{ row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="250">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleTest(row)">测试</el-button>
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="showModelDialog"
|
||||
:title="isEdit ? '编辑模型' : '添加模型'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="modelForm" label-width="120px">
|
||||
<el-form-item label="显示名称">
|
||||
<el-input v-model="modelForm.display_name" placeholder="如:GPT-3.5" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="modelForm.model_name" placeholder="如:gpt-3.5-turbo" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提供商">
|
||||
<el-select v-model="modelForm.provider" placeholder="请选择" :disabled="isEdit">
|
||||
<el-option label="OpenAI" value="openai" />
|
||||
<el-option label="智谱 AI" value="zhipu" />
|
||||
<el-option label="百度文心" value="baidu" />
|
||||
<el-option label="阿里通义" value="aliyun" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API 地址">
|
||||
<el-input v-model="modelForm.api_url" placeholder="https://api.openai.com/v1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="modelForm.api_key" type="password" show-password placeholder="sk-..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="温度">
|
||||
<el-slider v-model="modelForm.temperature" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大 Token 数">
|
||||
<el-input-number v-model="modelForm.max_tokens" :min="1" :max="4096" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设为默认">
|
||||
<el-switch v-model="modelForm.is_default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="modelForm.is_active" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showModelDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 测试对话框 -->
|
||||
<el-dialog
|
||||
v-model="showTestDialog"
|
||||
title="测试模型"
|
||||
width="500px"
|
||||
>
|
||||
<el-form>
|
||||
<el-form-item label="测试提示词">
|
||||
<el-input
|
||||
v-model="testPrompt"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入测试内容,如:请写一条关于春天的评论"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showTestDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleRunTest" :loading="testing">测试</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { aiModelApi } from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
const modelList = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const showModelDialog = ref(false)
|
||||
const showTestDialog = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentModelId = ref(null)
|
||||
const testPrompt = ref('')
|
||||
|
||||
const modelForm = ref({
|
||||
display_name: '',
|
||||
model_name: '',
|
||||
provider: '',
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
is_default: false,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const testData = ref({})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await aiModelApi.getList()
|
||||
modelList.value = res || []
|
||||
} catch (error) {
|
||||
console.error('Load AI models error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/编辑
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
modelForm.value = {
|
||||
display_name: '',
|
||||
model_name: '',
|
||||
provider: '',
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
is_default: false,
|
||||
is_active: true
|
||||
}
|
||||
showModelDialog.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
currentModelId.value = row.id
|
||||
modelForm.value = { ...row }
|
||||
showModelDialog.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await aiModelApi.update(currentModelId.value, modelForm.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await aiModelApi.create(modelForm.value)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
showModelDialog.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Save model error:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除此模型配置吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await aiModelApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Delete model error:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试
|
||||
const handleTest = (row) => {
|
||||
currentModelId.value = row.id
|
||||
testPrompt.value = '请写一条简短的评论'
|
||||
showTestDialog.value = true
|
||||
}
|
||||
|
||||
const handleRunTest = async () => {
|
||||
testing.value = true
|
||||
try {
|
||||
const res = await aiModelApi.test({
|
||||
model_id: currentModelId.value,
|
||||
test_prompt: testPrompt.value
|
||||
})
|
||||
|
||||
testData.value = res
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`测试成功!消耗 ${res.tokens_used} tokens`)
|
||||
console.log('Test result:', res.content)
|
||||
} else {
|
||||
ElMessage.error(`测试失败:${res.error_message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Test model error:', error)
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getProviderLabel = (provider) => {
|
||||
const map = { openai: 'OpenAI', zhipu: '智谱 AI', baidu: '百度文心', aliyun: '阿里通义' }
|
||||
return map[provider] || provider
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-models {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
319
frontend/src/views/Dashboard.vue
Normal file
319
frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>核心指标</span>
|
||||
<el-button @click="refreshData" :loading="loading" circle>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 核心指标卡片 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="虚拟用户总数" :value="stats.core_stats?.total_users || 0">
|
||||
<template #suffix>
|
||||
<span style="font-size: 14px; color: #909399; margin-left: 8px;">
|
||||
(启用:{{ stats.core_stats?.active_users || 0 }} /
|
||||
禁用:{{ stats.core_stats?.disabled_users || 0 }})
|
||||
</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-statistic title="今日互动" :value="todayInteractions">
|
||||
<template #suffix>
|
||||
<span style="font-size: 14px; color: #909399; margin-left: 8px;">
|
||||
评论{{ stats.core_stats?.today_comments || 0 }} /
|
||||
回复{{ stats.core_stats?.today_replies || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-statistic title="今日 Token" :value="stats.core_stats?.today_tokens || 0">
|
||||
<template #suffix>
|
||||
<span style="font-size: 14px; color: #909399; margin-left: 8px;">
|
||||
剩余:{{ stats.core_stats?.remaining_tokens || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-statistic title="当月 Token" :value="stats.core_stats?.month_tokens || 0" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- Token 消耗图表 -->
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>近 30 日 Token 消耗</span>
|
||||
</template>
|
||||
<div ref="dailyChartRef" style="height: 300px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>近 12 月 Token 消耗</span>
|
||||
</template>
|
||||
<div ref="monthlyChartRef" style="height: 300px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 最近互动记录 -->
|
||||
<el-card style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<span>最近互动记录</span>
|
||||
</template>
|
||||
|
||||
<el-table :data="recentInteractions" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="virtual_user_id" label="用户 ID" width="100" />
|
||||
<el-table-column prop="interaction_type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getTypeTag(row.interaction_type)">
|
||||
{{ getTypeLabel(row.interaction_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getStatusTag(row.status)">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="execution_time" label="执行时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.execution_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { dashboardApi } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = ref({})
|
||||
const dailyChartRef = ref(null)
|
||||
const monthlyChartRef = ref(null)
|
||||
let dailyChart = null
|
||||
let monthlyChart = null
|
||||
|
||||
const todayInteractions = computed(() => {
|
||||
const s = stats.value.core_stats || {}
|
||||
return (s.today_comments || 0) + (s.today_replies || 0) +
|
||||
(s.today_likes || 0) + (s.today_favorites || 0) + (s.today_shares || 0)
|
||||
})
|
||||
|
||||
const recentInteractions = computed(() => {
|
||||
return stats.value.recent_interactions || []
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await dashboardApi.getStats()
|
||||
stats.value = res
|
||||
|
||||
// 渲染图表
|
||||
renderDailyChart()
|
||||
renderMonthlyChart()
|
||||
} catch (error) {
|
||||
console.error('Load dashboard data error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
loadData()
|
||||
ElMessage.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 渲染每日 Token 消耗图表
|
||||
const renderDailyChart = () => {
|
||||
if (!dailyChartRef.value) return
|
||||
|
||||
if (dailyChart) {
|
||||
dailyChart.dispose()
|
||||
}
|
||||
|
||||
dailyChart = echarts.init(dailyChartRef.value)
|
||||
|
||||
const dailyData = stats.value.daily_token_usages || []
|
||||
|
||||
dailyChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dailyData.map(item => item.date.substring(5)), // 只显示 MM-DD
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: 'Token 消耗',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: dailyData.map(item => item.tokens),
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
|
||||
])
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染每月 Token 消耗图表
|
||||
const renderMonthlyChart = () => {
|
||||
if (!monthlyChartRef.value) return
|
||||
|
||||
if (monthlyChart) {
|
||||
monthlyChart.dispose()
|
||||
}
|
||||
|
||||
monthlyChart = echarts.init(monthlyChartRef.value)
|
||||
|
||||
const monthlyData = stats.value.monthly_token_usages || []
|
||||
|
||||
monthlyChart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: monthlyData.map(item => item.month.substring(5)), // 只显示 MM
|
||||
axisTick: {
|
||||
alignWithLabel: true
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: 'Token 消耗',
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
data: monthlyData.map(item => item.tokens),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getTypeLabel = (type) => {
|
||||
const map = {
|
||||
'comment': '评论',
|
||||
'reply': '回复',
|
||||
'like': '点赞',
|
||||
'favorite': '收藏',
|
||||
'share': '转发'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const map = {
|
||||
'comment': 'primary',
|
||||
'reply': 'success',
|
||||
'like': 'warning',
|
||||
'favorite': 'info',
|
||||
'share': 'danger'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const map = {
|
||||
'pending': '待执行',
|
||||
'success': '成功',
|
||||
'failed': '失败',
|
||||
'retrying': '重试中'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const map = {
|
||||
'pending': 'info',
|
||||
'success': 'success',
|
||||
'failed': 'danger',
|
||||
'retrying': 'warning'
|
||||
}
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
dailyChart?.resize()
|
||||
monthlyChart?.resize()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
109
frontend/src/views/Interactions.vue
Normal file
109
frontend/src/views/Interactions.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="interactions">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>互动记录</span>
|
||||
</template>
|
||||
|
||||
<el-table :data="interactionList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="virtual_user_id" label="用户 ID" width="100" />
|
||||
<el-table-column prop="news_title" label="文章标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="interaction_type" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getTypeTag(row.interaction_type)">
|
||||
{{ getTypeLabel(row.interaction_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getStatusTag(row.status)">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tokens_used" label="Token 消耗" width="100" />
|
||||
<el-table-column prop="execution_time" label="执行时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.execution_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { interactionApi } from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const interactionList = ref([])
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await interactionApi.getList({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
interactionList.value = res.items || []
|
||||
pagination.value.total = res.total || 0
|
||||
} catch (error) {
|
||||
console.error('Load interactions error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const map = { comment: '评论', reply: '回复', like: '点赞', favorite: '收藏', share: '转发' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const map = { comment: 'primary', reply: 'success', like: 'warning', favorite: 'info', share: 'danger' }
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const map = { pending: '待执行', success: '成功', failed: '失败', retrying: '重试中' }
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const map = { pending: 'info', success: 'success', failed: 'danger', retrying: 'warning' }
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.interactions {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
244
frontend/src/views/Settings.vue
Normal file
244
frontend/src/views/Settings.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<el-row :gutter="20">
|
||||
<!-- 活动调度设置 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>活动调度设置</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="scheduleForm" label-width="140px">
|
||||
<el-form-item label="活动开始时间">
|
||||
<el-time-picker
|
||||
v-model="scheduleForm.task_start_hour"
|
||||
format="HH:mm"
|
||||
value-format="HH"
|
||||
placeholder="选择时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动结束时间">
|
||||
<el-time-picker
|
||||
v-model="scheduleForm.task_end_hour"
|
||||
format="HH:mm"
|
||||
value-format="HH"
|
||||
placeholder="选择时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最小间隔 (分钟)">
|
||||
<el-input-number v-model="scheduleForm.task_interval_min" :min="1" :max="60" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大间隔 (分钟)">
|
||||
<el-input-number v-model="scheduleForm.task_interval_max" :min="1" :max="120" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveSchedule" :loading="saving">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-form-item label="任务状态">
|
||||
<el-tag :type="schedulerStatus.is_running ? 'success' : 'info'">
|
||||
{{ schedulerStatus.is_running ? '运行中' : '已停止' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
v-if="!schedulerStatus.is_running"
|
||||
type="success"
|
||||
@click="handleStartScheduler"
|
||||
>
|
||||
启动任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="warning"
|
||||
@click="handleStopScheduler"
|
||||
>
|
||||
停止任务
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 限额设置 -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>限额设置</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="limitForm" label-width="160px">
|
||||
<el-form-item label="每日 Token 上限">
|
||||
<el-input-number v-model="limitForm.max_tokens_per_day" :min="0" :step="1000" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="单用户日评论上限">
|
||||
<el-input-number v-model="limitForm.max_comments_per_user_per_day" :min="0" :max="100" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="单用户日回复上限">
|
||||
<el-input-number v-model="limitForm.max_replies_per_user_per_day" :min="0" :max="50" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveLimits" :loading="saving">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 互动概率设置 -->
|
||||
<el-card style="margin-top: 20px;">
|
||||
<template #header>
|
||||
<span>互动概率设置</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="probabilityForm" label-width="160px" inline>
|
||||
<el-form-item label="点赞概率">
|
||||
<el-slider v-model="probabilityForm.like_probability" :min="0" :max="1" :step="0.1" show-input />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收藏概率">
|
||||
<el-slider v-model="probabilityForm.favorite_probability" :min="0" :max="1" :step="0.1" show-input />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="转发概率">
|
||||
<el-slider v-model="probabilityForm.share_probability" :min="0" :max="1" :step="0.1" show-input />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSaveProbabilities" :loading="saving">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { systemApi } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const saving = ref(false)
|
||||
const scheduleForm = ref({
|
||||
task_start_hour: '9',
|
||||
task_end_hour: '22',
|
||||
task_interval_min: 10,
|
||||
task_interval_max: 30
|
||||
})
|
||||
|
||||
const limitForm = ref({
|
||||
max_tokens_per_day: 10000,
|
||||
max_comments_per_user_per_day: 20,
|
||||
max_replies_per_user_per_day: 10
|
||||
})
|
||||
|
||||
const probabilityForm = ref({
|
||||
like_probability: 0.8,
|
||||
favorite_probability: 0.5,
|
||||
share_probability: 0.3
|
||||
})
|
||||
|
||||
const schedulerStatus = ref({
|
||||
is_running: false,
|
||||
jobs: []
|
||||
})
|
||||
|
||||
// 加载配置
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const [schedule, limits, probabilities, status] = await Promise.all([
|
||||
systemApi.getSchedule(),
|
||||
systemApi.getLimits(),
|
||||
systemApi.getProbabilities(),
|
||||
systemApi.getSchedulerStatus()
|
||||
])
|
||||
|
||||
scheduleForm.value = schedule
|
||||
limitForm.value = limits
|
||||
probabilityForm.value = probabilities
|
||||
schedulerStatus.value = status
|
||||
} catch (error) {
|
||||
console.error('Load config error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存调度配置
|
||||
const handleSaveSchedule = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
await systemApi.updateSchedule(scheduleForm.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
console.error('Save schedule error:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存限额配置
|
||||
const handleSaveLimits = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
await systemApi.updateLimits(limitForm.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
console.error('Save limits error:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存概率配置
|
||||
const handleSaveProbabilities = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
// TODO: 实现概率配置 API
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
console.error('Save probabilities error:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 启动调度器
|
||||
const handleStartScheduler = async () => {
|
||||
try {
|
||||
await systemApi.startScheduler()
|
||||
ElMessage.success('任务已启动')
|
||||
loadConfig()
|
||||
} catch (error) {
|
||||
console.error('Start scheduler error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 停止调度器
|
||||
const handleStopScheduler = async () => {
|
||||
try {
|
||||
await systemApi.stopScheduler()
|
||||
ElMessage.success('任务已停止')
|
||||
loadConfig()
|
||||
} catch (error) {
|
||||
console.error('Stop scheduler error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
373
frontend/src/views/VirtualUsers.vue
Normal file
373
frontend/src/views/VirtualUsers.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div class="virtual-users">
|
||||
<el-card>
|
||||
<!-- 操作栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名或昵称"
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
clearable
|
||||
@clear="loadData"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 120px;" @change="loadData">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="showGenerateDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
批量生成
|
||||
</el-button>
|
||||
|
||||
<el-button type="success" @click="triggerImport">
|
||||
<el-icon><Upload /></el-icon>
|
||||
Excel 导入
|
||||
</el-button>
|
||||
|
||||
<input
|
||||
ref="importFileInput"
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
style="display: none"
|
||||
@change="handleImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-table
|
||||
:data="userList"
|
||||
v-loading="loading"
|
||||
style="width: 100%; margin-top: 20px;"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="avatar_url" label="头像" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :src="row.avatar_url" :size="40" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nickname" label="昵称" width="150" />
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="writing_style" label="写作风格" width="120" />
|
||||
<el-table-column prop="activity_level" label="活跃度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="getActivityLevelTag(row.activity_level)">
|
||||
{{ getActivityLevelLabel(row.activity_level) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.status === 'active' ? 'success' : 'info'">
|
||||
{{ row.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_interactions" label="互动次数" width="100" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="loadData"
|
||||
@size-change="loadData"
|
||||
style="margin-top: 20px; justify-content: flex-end;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 批量生成对话框 -->
|
||||
<el-dialog
|
||||
v-model="showGenerateDialog"
|
||||
title="批量生成虚拟用户"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="generateForm" label-width="120px">
|
||||
<el-form-item label="生成数量">
|
||||
<el-input-number v-model="generateForm.count" :min="1" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="写作风格">
|
||||
<el-select v-model="generateForm.writing_styles" multiple placeholder="不选则随机">
|
||||
<el-option label="幽默风趣" value="幽默风趣" />
|
||||
<el-option label="严肃理性" value="严肃理性" />
|
||||
<el-option label="文艺清新" value="文艺清新" />
|
||||
<el-option label="吐槽犀利" value="吐槽犀利" />
|
||||
<el-option label="感性温暖" value="感性温暖" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="活跃度">
|
||||
<el-checkbox-group v-model="generateForm.activity_levels">
|
||||
<el-checkbox label="low">低</el-checkbox>
|
||||
<el-checkbox label="medium">中</el-checkbox>
|
||||
<el-checkbox label="high">高</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="AI 人格描述">
|
||||
<el-switch v-model="generateForm.generate_persona" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showGenerateDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleGenerate" :loading="generating">生成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="showEditDialog"
|
||||
title="编辑虚拟用户"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="editForm.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="editForm.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="editForm.password" type="password" show-password placeholder="不修改则留空" />
|
||||
</el-form-item>
|
||||
<el-form-item label="写作风格">
|
||||
<el-select v-model="editForm.writing_style">
|
||||
<el-option label="幽默风趣" value="幽默风趣" />
|
||||
<el-option label="严肃理性" value="严肃理性" />
|
||||
<el-option label="文艺清新" value="文艺清新" />
|
||||
<el-option label="吐槽犀利" value="吐槽犀利" />
|
||||
<el-option label="感性温暖" value="感性温暖" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="活跃度">
|
||||
<el-radio-group v-model="editForm.activity_level">
|
||||
<el-radio label="low">低</el-radio>
|
||||
<el-radio label="medium">中</el-radio>
|
||||
<el-radio label="high">高</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="editForm.status">
|
||||
<el-radio label="active">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitEdit" :loading="saving">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { virtualUserApi } from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const generating = ref(false)
|
||||
const saving = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref('')
|
||||
const userList = ref([])
|
||||
const showGenerateDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const importFileInput = ref(null)
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const generateForm = ref({
|
||||
count: 10,
|
||||
writing_styles: [],
|
||||
activity_levels: ['low', 'medium', 'high'],
|
||||
generate_persona: true
|
||||
})
|
||||
|
||||
const editForm = ref({})
|
||||
const currentUserId = ref(null)
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
}
|
||||
|
||||
if (searchKeyword.value) {
|
||||
params.search = searchKeyword.value
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
|
||||
const res = await virtualUserApi.getList(params)
|
||||
userList.value = res.items || []
|
||||
pagination.value.total = res.total || 0
|
||||
} catch (error) {
|
||||
console.error('Load user list error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.value.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 批量生成
|
||||
const handleGenerate = async () => {
|
||||
generating.value = true
|
||||
try {
|
||||
await virtualUserApi.generate(generateForm.value)
|
||||
ElMessage.success(`成功生成 ${generateForm.value.count} 个虚拟用户`)
|
||||
showGenerateDialog.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Generate users error:', error)
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Excel 导入
|
||||
const triggerImport = () => {
|
||||
importFileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleImport = async (event) => {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const res = await virtualUserApi.import(file, true)
|
||||
ElMessage.success(`导入完成:成功${res.success_count}个,失败${res.failed_count}个`)
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Import users error:', error)
|
||||
} finally {
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
currentUserId.value = row.id
|
||||
editForm.value = { ...row }
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
// 提交编辑
|
||||
const handleSubmitEdit = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
const data = { ...editForm.value }
|
||||
if (!data.password) {
|
||||
delete data.password
|
||||
}
|
||||
await virtualUserApi.update(currentUserId.value, data)
|
||||
ElMessage.success('保存成功')
|
||||
showEditDialog.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除虚拟用户 "${row.nickname}" 吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
await virtualUserApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getActivityLevelLabel = (level) => {
|
||||
const map = { low: '低', medium: '中', high: '高' }
|
||||
return map[level] || level
|
||||
}
|
||||
|
||||
const getActivityLevelTag = (level) => {
|
||||
const map = { low: 'info', medium: '', high: 'success' }
|
||||
return map[level] || ''
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.virtual-users {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-left, .toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user