1.0.0初始化源代码

This commit is contained in:
yuqianqian10204095yu
2026-03-23 15:40:36 +08:00
parent f13ecb3bba
commit cebc0a288f
53 changed files with 5300 additions and 0 deletions

22
frontend/src/App.vue Normal file
View 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
View 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')
}

View 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
View 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')

View 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

View 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>

View 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>

View 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>

View 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>

View 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>