|
|
|
@@ -0,0 +1,300 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="page-container">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<div style="display:flex;align-items:center;gap:12px">
|
|
|
|
|
<span class="page-title">数字分身管理</span>
|
|
|
|
|
<el-tag type="info">共 {{ total }} 个分身</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Filters -->
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
<el-input v-model="keyword" placeholder="搜索分身名称" clearable @change="loadAvatars" style="width:200px">
|
|
|
|
|
<template #prefix><el-icon><Search /></el-icon></template>
|
|
|
|
|
</el-input>
|
|
|
|
|
<el-select v-model="filterStatus" placeholder="状态" clearable @change="loadAvatars" style="width:130px">
|
|
|
|
|
<el-option label="运行中" value="active" />
|
|
|
|
|
<el-option label="已关机" value="inactive" />
|
|
|
|
|
<el-option label="训练中" value="training" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button @click="resetFilters">重置</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Table -->
|
|
|
|
|
<el-table :data="avatars" v-loading="loading" :row-class-name="rowClassName" class="data-table">
|
|
|
|
|
<el-table-column label="分身" width="240">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<div style="display:flex;align-items:center;gap:10px">
|
|
|
|
|
<el-avatar :size="40" :src="row.photo_url || ''" style="background:#30363d;flex-shrink:0">
|
|
|
|
|
{{ row.emoji || '🤖' }}
|
|
|
|
|
</el-avatar>
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-weight:600;font-size:13px">{{ row.name }}</div>
|
|
|
|
|
<div v-if="row.display_name" style="font-size:11px;color:var(--color-text-muted)">{{ row.display_name }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="归属用户" width="160">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span v-if="row.owner_nickname">{{ row.owner_nickname }}</span>
|
|
|
|
|
<span v-else style="color:var(--color-text-muted)">未绑定</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="状态" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="statusType[row.status]" size="small">{{ statusLabel[row.status] }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="Token 余额" width="120" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span style="font-weight:600">{{ row.token_balance?.toLocaleString() ?? 0 }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="创建时间" width="160">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span style="font-size:12px;color:var(--color-text-muted)">{{ row.created_at ? new Date(row.created_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false}) : '--' }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="开关机" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tooltip :content="row.status === 'active' ? '点击关机' : '点击开机'" placement="top">
|
|
|
|
|
<el-switch
|
|
|
|
|
:model-value="row.status === 'active'"
|
|
|
|
|
@change="toggleStatus(row)"
|
|
|
|
|
:loading="row._switching"
|
|
|
|
|
active-color="var(--color-accent-green)"
|
|
|
|
|
inactive-color="var(--color-accent-orange)"
|
|
|
|
|
inline-prompt
|
|
|
|
|
active-text="开"
|
|
|
|
|
inactive-text="关"
|
|
|
|
|
/>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="100" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button size="small" @click="openDetail(row)">详情</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="page" v-model:page-size="pageSize"
|
|
|
|
|
:total="total" :page-sizes="[10, 20, 50]"
|
|
|
|
|
layout="total, sizes, prev, pager, next"
|
|
|
|
|
@change="loadAvatars" style="margin-top:16px;justify-content:flex-end;display:flex"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- Detail Dialog -->
|
|
|
|
|
<el-dialog v-model="detailVisible" title="数字分身详情" width="640px">
|
|
|
|
|
<div v-if="detailAvatar" class="detail-panel">
|
|
|
|
|
<!-- Header with avatar -->
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
<el-avatar :size="72" :src="detailAvatar.photo_url || ''" style="background:#30363d">
|
|
|
|
|
<span style="font-size:32px">{{ detailAvatar.emoji || '🤖' }}</span>
|
|
|
|
|
</el-avatar>
|
|
|
|
|
<div style="margin-left:16px">
|
|
|
|
|
<div class="detail-name">{{ detailAvatar.name }}</div>
|
|
|
|
|
<div v-if="detailAvatar.display_name" class="detail-display-name">{{ detailAvatar.display_name }}</div>
|
|
|
|
|
<el-tag :type="statusType[detailAvatar.status]" size="small" style="margin-top:4px">
|
|
|
|
|
{{ statusLabel[detailAvatar.status] }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-divider />
|
|
|
|
|
|
|
|
|
|
<!-- Basic Info -->
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
<div class="detail-section-title">基本信息</div>
|
|
|
|
|
<div class="detail-grid">
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">名称</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">显示名称</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.display_name || '--' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">归属用户</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.owner_nickname || '未绑定' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">Token 余额</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.token_balance?.toLocaleString() ?? 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">创建时间</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.created_at ? new Date(detailAvatar.created_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false}) : '--' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="detail-label">更新时间</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.updated_at ? new Date(detailAvatar.updated_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false}) : '--' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-divider />
|
|
|
|
|
|
|
|
|
|
<!-- Description -->
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
<div class="detail-section-title">描述</div>
|
|
|
|
|
<div class="detail-value" style="line-height:1.6">{{ detailAvatar.description || '暂无描述' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-divider />
|
|
|
|
|
|
|
|
|
|
<!-- Config -->
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
<div class="detail-section-title">配置参数</div>
|
|
|
|
|
<div v-if="hasConfig" class="detail-grid">
|
|
|
|
|
<div class="detail-item" v-if="detailAvatar.config.replyStyle">
|
|
|
|
|
<span class="detail-label">回复风格</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.config.replyStyle }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item" v-if="detailAvatar.config.creativity">
|
|
|
|
|
<span class="detail-label">创造力</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.config.creativity }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item" v-if="detailAvatar.config.rigor">
|
|
|
|
|
<span class="detail-label">严谨度</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.config.rigor }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item" v-if="detailAvatar.config.humor">
|
|
|
|
|
<span class="detail-label">幽默度</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.config.humor }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item" v-if="detailAvatar.config.responseLength">
|
|
|
|
|
<span class="detail-label">回复长度</span>
|
|
|
|
|
<span class="detail-value">{{ detailAvatar.config.responseLength }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else style="color:var(--color-text-muted);font-size:13px">暂无配置</div>
|
|
|
|
|
<div v-if="detailAvatar.config.systemPrompt" style="margin-top:12px">
|
|
|
|
|
<span class="detail-label" style="display:block;margin-bottom:4px">系统提示词</span>
|
|
|
|
|
<pre class="system-prompt">{{ detailAvatar.config.systemPrompt }}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="detailVisible = false">关闭</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { getAvatars, updateAvatarStatus } from '@/api'
|
|
|
|
|
|
|
|
|
|
const avatars = ref([])
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const page = ref(1)
|
|
|
|
|
const pageSize = ref(20)
|
|
|
|
|
const keyword = ref('')
|
|
|
|
|
const filterStatus = ref('')
|
|
|
|
|
|
|
|
|
|
const statusType = { active: 'success', inactive: 'info', training: 'warning' }
|
|
|
|
|
const statusLabel = { active: '运行中', inactive: '已关机', training: '训练中' }
|
|
|
|
|
|
|
|
|
|
const detailVisible = ref(false)
|
|
|
|
|
const detailAvatar = ref(null)
|
|
|
|
|
const hasConfig = computed(() => {
|
|
|
|
|
if (!detailAvatar.value || !detailAvatar.value.config) return false
|
|
|
|
|
const c = detailAvatar.value.config
|
|
|
|
|
return c.replyStyle || c.creativity || c.rigor || c.humor || c.responseLength
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function loadAvatars() {
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const params = { page: page.value, page_size: pageSize.value }
|
|
|
|
|
if (keyword.value) params.keyword = keyword.value
|
|
|
|
|
if (filterStatus.value) params.status = filterStatus.value
|
|
|
|
|
const res = await getAvatars(params)
|
|
|
|
|
avatars.value = res.data?.items || []
|
|
|
|
|
total.value = res.data?.total || 0
|
|
|
|
|
} finally { loading.value = false }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetFilters() {
|
|
|
|
|
keyword.value = ''
|
|
|
|
|
filterStatus.value = ''
|
|
|
|
|
page.value = 1
|
|
|
|
|
loadAvatars()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function toggleStatus(row) {
|
|
|
|
|
const newStatus = row.status === 'active' ? 'inactive' : 'active'
|
|
|
|
|
row._switching = true
|
|
|
|
|
try {
|
|
|
|
|
await updateAvatarStatus(row.id, newStatus)
|
|
|
|
|
row.status = newStatus
|
|
|
|
|
const msg = newStatus === 'active' ? '已开机' : '已关机'
|
|
|
|
|
ElMessage.success(msg)
|
|
|
|
|
} catch {
|
|
|
|
|
ElMessage.error('状态切换失败')
|
|
|
|
|
} finally {
|
|
|
|
|
row._switching = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openDetail(row) {
|
|
|
|
|
detailAvatar.value = row
|
|
|
|
|
detailVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rowClassName({ rowIndex }) {
|
|
|
|
|
return rowIndex % 2 === 0 ? 'row-dark' : 'row-darker'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => { loadAvatars(); window.addEventListener('page-refresh', loadAvatars) })
|
|
|
|
|
onUnmounted(() => window.removeEventListener('page-refresh', loadAvatars))
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.filter-bar { display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
|
|
|
.data-table { border-radius: 8px; overflow: hidden; }
|
|
|
|
|
|
|
|
|
|
/* Detail panel */
|
|
|
|
|
.detail-header { display: flex; align-items: center; }
|
|
|
|
|
.detail-name { font-size: 18px; font-weight: 700; }
|
|
|
|
|
.detail-display-name { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; }
|
|
|
|
|
.detail-section { margin-bottom: 4px; }
|
|
|
|
|
.detail-section-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
|
|
|
|
|
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
|
|
|
.detail-item { display: flex; align-items: center; gap: 8px; }
|
|
|
|
|
.detail-label { font-size: 12px; color: var(--color-text-muted); white-space: nowrap; width: 70px; flex-shrink: 0; }
|
|
|
|
|
.detail-value { font-size: 13px; }
|
|
|
|
|
.system-prompt {
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Dark theme rows */
|
|
|
|
|
:deep(.row-dark td) { background: var(--color-bg-card) !important; }
|
|
|
|
|
:deep(.row-darker td) { background: var(--color-bg-secondary) !important; }
|
|
|
|
|
:deep(.el-table__row:hover td) { background: rgba(88,166,255,0.08) !important; }
|
|
|
|
|
:deep(.el-table__header-wrapper th) {
|
|
|
|
|
background: var(--color-bg) !important;
|
|
|
|
|
color: var(--color-text-muted) !important;
|
|
|
|
|
border-bottom: 2px solid var(--color-border) !important;
|
|
|
|
|
font-size: 12px !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-table__body-wrapper) { background: transparent !important; }
|
|
|
|
|
:deep(.el-table) { background: transparent !important; }
|
|
|
|
|
</style>
|