615 lines
26 KiB
Vue
615 lines
26 KiB
Vue
<template>
|
||
<div class="page-container">
|
||
<!-- Toolbar -->
|
||
<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 style="display:flex;gap:8px">
|
||
<el-button @click="downloadTemplate" size="small">
|
||
<el-icon><Download /></el-icon> 下载模板
|
||
</el-button>
|
||
<el-upload :show-file-list="false" accept=".xlsx,.xls" :before-upload="handleImport">
|
||
<el-tooltip content="仅账号和密码为必填,昵称为空时自动生成" placement="top">
|
||
<el-button size="small" type="warning">
|
||
<el-icon><Upload /></el-icon> 批量导入
|
||
</el-button>
|
||
</el-tooltip>
|
||
</el-upload>
|
||
<el-button size="small" @click="handleExport">
|
||
<el-icon><Download /></el-icon> 导出
|
||
</el-button>
|
||
<el-popconfirm title="将删除重复账号,只保留最早导入的一条,确认?" @confirm="handleDeduplicate" confirm-button-text="确认去重" cancel-button-text="取消">
|
||
<template #reference>
|
||
<el-button size="small" type="warning">
|
||
<el-icon><CopyDocument /></el-icon> 去重
|
||
</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
<el-popconfirm title="将对所有未登录/登录失效的用户执行登录,确认?" @confirm="handleLoginAll" confirm-button-text="确认" cancel-button-text="取消">
|
||
<template #reference>
|
||
<el-button size="small" type="success" :loading="loginAllLoading">
|
||
<el-icon><Connection /></el-icon> 一键登录全部
|
||
</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
<el-popconfirm title="确定要登出所有虚拟用户吗?" @confirm="handleLogoutAll">
|
||
<template #reference>
|
||
<el-button size="small" type="info" :loading="logoutAllLoading">
|
||
<el-icon><SwitchButton /></el-icon> 一键登出全部
|
||
</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
<el-tooltip content="从目标平台同步所有已登录用户的昵称/真实姓名/性别/头像" placement="top">
|
||
<el-button size="small" type="info" :loading="syncLoading" @click="handleSyncAll">
|
||
<el-icon><Refresh /></el-icon> 同步用户信息
|
||
</el-button>
|
||
</el-tooltip>
|
||
<el-button type="primary" size="small" @click="openCreateDialog">
|
||
<el-icon><Plus /></el-icon> 新增用户
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="filter-bar">
|
||
<el-input v-model="filters.keyword" placeholder="搜索昵称/账号" clearable @change="loadUsers" style="width:200px">
|
||
<template #prefix><el-icon><Search /></el-icon></template>
|
||
</el-input>
|
||
<el-select v-model="filters.status" placeholder="用户状态" clearable @change="loadUsers" style="width:130px">
|
||
<el-option v-for="(label,val) in statusMap" :key="val" :label="label" :value="Number(val)" />
|
||
</el-select>
|
||
<el-select v-model="filters.is_enabled" placeholder="启用状态" clearable @change="loadUsers" style="width:120px">
|
||
<el-option label="已启用" :value="1" /><el-option label="已禁用" :value="0" />
|
||
</el-select>
|
||
<el-button @click="resetFilters">重置</el-button>
|
||
</div>
|
||
|
||
<!-- Batch actions -->
|
||
<div v-if="selectedIds.length" class="batch-bar">
|
||
<span class="batch-info">已选 {{ selectedIds.length }} 项</span>
|
||
<el-button size="small" type="success" @click="batchAction('enable')">批量启用</el-button>
|
||
<el-button size="small" type="warning" @click="batchAction('disable')">批量禁用</el-button>
|
||
<el-button size="small" @click="batchAction('logout')">批量登出</el-button>
|
||
<el-button size="small" type="danger" @click="batchAction('delete')">批量删除</el-button>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<el-table
|
||
:data="users" v-loading="loading" :row-class-name="rowClassName"
|
||
@selection-change="selectedIds = $event.map(r => r.id)"
|
||
class="data-table"
|
||
>
|
||
<el-table-column type="selection" width="48" />
|
||
<el-table-column label="用户" width="200">
|
||
<template #default="{ row }">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<el-avatar :size="36" :src="row.avatar_url || ''" style="background:#30363d;flex-shrink:0">
|
||
{{ row.nickname?.[0] }}
|
||
</el-avatar>
|
||
<div>
|
||
<div style="font-weight:600;font-size:13px">{{ row.nickname }}</div>
|
||
<div style="font-size:11px;color:var(--color-text-muted)">{{ row.account }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusType[row.status]" size="small">{{ row.status_label }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="活跃度" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="activityType[row.activity_level]" size="small" effect="plain">{{ row.activity_label }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="人格" min-width="140">
|
||
<template #default="{ row }">
|
||
<div v-if="row.personality" style="display:flex;gap:4px;flex-wrap:wrap">
|
||
<el-tag size="small" effect="plain">{{ row.personality.character_type }}</el-tag>
|
||
<el-tag size="small" effect="plain" type="warning">{{ row.personality.language_style }}</el-tag>
|
||
<el-tag v-for="t in (row.personality.interest_tags||[]).slice(0,2)" :key="t" size="small" effect="plain" type="info">{{ t }}</el-tag>
|
||
</div>
|
||
<span v-else style="color:var(--color-text-muted);font-size:12px">未生成</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="今日评论" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<span :style="row.today_comment_count >= row.daily_comment_limit ? 'color:var(--color-accent-red)' : ''">
|
||
{{ row.today_comment_count }}/{{ row.daily_comment_limit }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="累计互动" width="90" align="center" prop="total_interactions" />
|
||
<el-table-column label="最后互动" width="145">
|
||
<template #default="{ row }">
|
||
<span style="font-size:12px;color:var(--color-text-muted)">{{ row.last_interact_at ? new Date(row.last_interact_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai',hour12:false}) : '--' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="启用" width="70" align="center">
|
||
<template #default="{ row }">
|
||
<el-switch :model-value="row.is_enabled === 1" @change="toggleEnabled(row)" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="220" fixed="right">
|
||
<template #default="{ row }">
|
||
<div style="display:flex;gap:6px;align-items:center;flex-wrap:nowrap;">
|
||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||
<el-button size="small" type="success" v-if="row.status !== 2" @click="doLogin(row)">登录</el-button>
|
||
<el-button size="small" type="warning" v-else @click="doLogout(row)">登出</el-button>
|
||
<el-dropdown size="small" trigger="click">
|
||
<el-button size="small">更多<el-icon style="margin-left:2px"><ArrowDown /></el-icon></el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item @click="openPersonality(row)">🧠 人格设置</el-dropdown-item>
|
||
<el-dropdown-item @click="doDelete(row)" style="color:#f85149">🗑️ 删除用户</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="page" v-model:page-size="pageSize"
|
||
:total="total" :page-sizes="[20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@change="loadUsers" style="margin-top:16px;justify-content:flex-end;display:flex"
|
||
/>
|
||
|
||
<!-- Create/Edit Dialog -->
|
||
<el-dialog v-model="dialogVisible" :title="editUser ? '编辑用户' : '新增虚拟用户'" width="560px">
|
||
<el-form :model="form" label-width="100px" :rules="rules" ref="formRef">
|
||
<el-form-item label="昵称">
|
||
<el-input v-model="form.nickname" placeholder="选填,留空自动生成" />
|
||
</el-form-item>
|
||
<el-form-item label="平台账号" prop="account">
|
||
<el-input v-model="form.account" placeholder="新闻平台登录账号" :disabled="!!editUser" />
|
||
</el-form-item>
|
||
<el-form-item label="登录密码" :prop="editUser ? '' : 'password'">
|
||
<el-input v-model="form.password" type="password" :placeholder="editUser ? '不修改请留空' : '登录密码'" show-password />
|
||
</el-form-item>
|
||
<el-form-item label="头像">
|
||
<div style="display:flex;gap:10px;align-items:flex-start;width:100%">
|
||
<el-avatar :size="50" :src="form.avatar_url" style="flex-shrink:0">
|
||
<span>{{ (form.nickname||'?')[0] }}</span>
|
||
</el-avatar>
|
||
<div style="flex:1">
|
||
<el-input v-model="form.avatar_url" placeholder="头像图片链接" style="margin-bottom:6px" />
|
||
<el-upload v-if="editUser" :show-file-list="false" :before-upload="handleAvatarUpload" accept="image/*">
|
||
<el-button size="small" type="primary" plain :loading="avatarUploading">📷 上传头像</el-button>
|
||
</el-upload>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="真实姓名">
|
||
<el-input v-model="form.real_name" placeholder="可选" />
|
||
</el-form-item>
|
||
<el-form-item label="平台昵称">
|
||
<el-input v-model="form.nickname" placeholder="同步到目标平台的昵称" />
|
||
<div style="font-size:11px;color:var(--color-text-muted);margin-top:4px">昵称将同步到目标平台个人中心显示</div>
|
||
</el-form-item>
|
||
<el-form-item label="性别">
|
||
<el-radio-group v-model="form.sex">
|
||
<el-radio :value="0">未知</el-radio>
|
||
<el-radio :value="1">男</el-radio>
|
||
<el-radio :value="2">女</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="个人简介">
|
||
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="个人简介(可同步到平台)" maxlength="100" show-word-limit />
|
||
</el-form-item>
|
||
<el-form-item label="邮箱">
|
||
<el-input v-model="form.email" placeholder="邮箱地址(可同步到平台)" />
|
||
</el-form-item>
|
||
<el-form-item label="活跃度">
|
||
<el-radio-group v-model="form.activity_level">
|
||
<el-radio :value="0">低 (3-5次/天)</el-radio>
|
||
<el-radio :value="1">中 (8-15次/天)</el-radio>
|
||
<el-radio :value="2">高 (20-30次/天)</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="评论上限">
|
||
<el-input-number v-model="form.daily_comment_limit" :min="1" :max="100" />
|
||
<span style="margin-left:8px;font-size:12px;color:var(--color-text-muted)">次/天</span>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||
{{ editUser ? '保存' : '创建并生成人格' }}
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Personality Dialog -->
|
||
<el-dialog v-model="personalityVisible" title="AI人格管理" width="560px">
|
||
<div v-if="currentPersonality" class="personality-panel">
|
||
<div class="p-header">
|
||
<span class="p-user">{{ currentUser?.nickname }}</span>
|
||
<el-button size="small" type="primary" @click="regenPersonality" :loading="regenLoading">
|
||
<el-icon><RefreshRight /></el-icon> 重新生成
|
||
</el-button>
|
||
</div>
|
||
<div class="p-grid">
|
||
<div class="p-item"><span class="p-key">性格类型</span>
|
||
<el-select v-model="currentPersonality.character_type" size="small" style="width:120px">
|
||
<el-option v-for="c in characters" :key="c" :label="c" :value="c" />
|
||
</el-select>
|
||
</div>
|
||
<div class="p-item"><span class="p-key">语言风格</span>
|
||
<el-select v-model="currentPersonality.language_style" size="small" style="width:120px">
|
||
<el-option v-for="s in langStyles" :key="s" :label="s" :value="s" />
|
||
</el-select>
|
||
</div>
|
||
<div class="p-item"><span class="p-key">互动倾向</span>
|
||
<el-select v-model="currentPersonality.interact_tendency" size="small" style="width:120px">
|
||
<el-option v-for="t in tendencies" :key="t" :label="t" :value="t" />
|
||
</el-select>
|
||
</div>
|
||
<div class="p-item"><span class="p-key">字数范围</span>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<el-input-number v-model="currentPersonality.word_count_min" :min="10" :max="200" size="small" style="width:90px" />
|
||
<span>~</span>
|
||
<el-input-number v-model="currentPersonality.word_count_max" :min="30" :max="500" size="small" style="width:90px" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="p-item" style="margin-top:12px">
|
||
<span class="p-key">兴趣偏好</span>
|
||
<el-select v-model="currentPersonality.interest_tags" multiple size="small" style="width:100%">
|
||
<el-option v-for="t in interestTags" :key="t" :label="t" :value="t" />
|
||
</el-select>
|
||
</div>
|
||
<div class="p-item" style="margin-top:12px">
|
||
<span class="p-key">人格描述</span>
|
||
<el-input v-model="currentPersonality.personality_desc" type="textarea" :rows="3" size="small" />
|
||
</div>
|
||
</div>
|
||
<div v-else class="p-empty">
|
||
<el-empty description="尚未生成人格">
|
||
<el-button type="primary" @click="regenPersonality">立即生成</el-button>
|
||
</el-empty>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="personalityVisible = false">关闭</el-button>
|
||
<el-button v-if="currentPersonality" type="primary" @click="savePersonality" :loading="savingPersonality">保存人格</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
getUsers, createUser, updateUser, deleteUser, batchUserAction,
|
||
loginUser, logoutUser, generatePersonality, updatePersonality,
|
||
downloadTemplate as dlTemplate, importUsers, exportUsers, deduplicateUsers, loginAllUsers, syncAllProfiles,
|
||
uploadAvatar
|
||
} from '@/api'
|
||
|
||
const users = ref([])
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
const page = ref(1)
|
||
const pageSize = ref(20)
|
||
const selectedIds = ref([])
|
||
const filters = reactive({ keyword: '', status: null, is_enabled: null })
|
||
|
||
const dialogVisible = ref(false)
|
||
const submitting = ref(false)
|
||
const editUser = ref(null)
|
||
const formRef = ref()
|
||
const form = reactive({ nickname: '', account: '', password: '', avatar_url: '', activity_level: 1, daily_comment_limit: 10, remark: '' })
|
||
const rules = {
|
||
account: [{ required: true, message: '请输入平台账号(必填)' }],
|
||
password: [{ required: true, min: 6, message: '密码至少6位(必填)' }],
|
||
}
|
||
|
||
const personalityVisible = ref(false)
|
||
const currentUser = ref(null)
|
||
const currentPersonality = ref(null)
|
||
const regenLoading = ref(false)
|
||
const savingPersonality = ref(false)
|
||
|
||
const statusMap = { 0: '未登录', 1: '登录中', 2: '已登录', 3: '登录失效', 4: '封禁' }
|
||
const statusType = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'danger' }
|
||
const activityType = { 0: 'info', 1: 'warning', 2: 'success' }
|
||
const characters = ['开朗','内敛','毒舌','温和','理性','感性','幽默','严谨']
|
||
const langStyles = ['严肃','幽默','文艺','吐槽','口语化','学术','简洁','丰富']
|
||
const tendencies = ['爱评论','爱点赞','爱收藏','潜水','爱转发','爱回复']
|
||
const interestTags = ['科技','财经','娱乐','体育','政治','文化','教育','医疗','汽车','房产','旅游','美食','军事','国际','环保','农业']
|
||
|
||
async function loadUsers() {
|
||
loading.value = true
|
||
try {
|
||
const res = await getUsers({ page: page.value, page_size: pageSize.value, ...filters })
|
||
users.value = res.data?.items || []
|
||
total.value = res.data?.total || 0
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
function resetFilters() {
|
||
Object.assign(filters, { keyword: '', status: null, is_enabled: null })
|
||
loadUsers()
|
||
}
|
||
|
||
function openCreateDialog() {
|
||
editUser.value = null
|
||
Object.assign(form, { nickname: '', account: '', password: '', avatar_url: '', real_name: '', sex: 0, description: '', email: '', activity_level: 1, daily_comment_limit: 10, remark: '' })
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function openEdit(row) {
|
||
editUser.value = row
|
||
Object.assign(form, { nickname: row.nickname, account: row.account, password: '', avatar_url: row.avatar_url || '', real_name: row.real_name || '', sex: row.sex ?? 0, description: row.description || '', email: row.email || '', activity_level: row.activity_level, daily_comment_limit: row.daily_comment_limit, remark: row.remark || '' })
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function submitForm() {
|
||
await formRef.value.validate()
|
||
submitting.value = true
|
||
try {
|
||
if (editUser.value) {
|
||
const data = { ...form }
|
||
if (!data.password) delete data.password
|
||
data.sync_to_platform = true // 自动同步到目标平台
|
||
const res = await updateUser(editUser.value.id, data)
|
||
if (res.code === 200 || res.code === 206) {
|
||
// 局部刷新:只更新当前行数据,不刷新整个列表
|
||
const idx = users.value.findIndex(u => u.id === editUser.value.id)
|
||
if (idx !== -1) {
|
||
const updated = { ...users.value[idx], ...{
|
||
nickname: form.nickname || users.value[idx].nickname,
|
||
avatar_url: form.avatar_url,
|
||
real_name: form.real_name,
|
||
sex: form.sex,
|
||
activity_level: form.activity_level,
|
||
daily_comment_limit: form.daily_comment_limit,
|
||
remark: form.remark,
|
||
}}
|
||
users.value.splice(idx, 1, updated)
|
||
}
|
||
if (res.code === 206) {
|
||
ElMessage.warning(res.message || '本地已保存,平台同步失败')
|
||
} else {
|
||
ElMessage.success('更新成功')
|
||
}
|
||
} else {
|
||
ElMessage.error(res.message || '更新失败')
|
||
}
|
||
} else {
|
||
await createUser(form)
|
||
ElMessage.success('用户创建成功,AI人格已自动生成')
|
||
loadUsers()
|
||
}
|
||
dialogVisible.value = false
|
||
} finally { submitting.value = false }
|
||
}
|
||
|
||
async function doDelete(row) {
|
||
await ElMessageBox.confirm(`确认删除用户 "${row.nickname}"?`, '确认', { type: 'warning' })
|
||
await deleteUser(row.id)
|
||
ElMessage.success('删除成功')
|
||
loadUsers()
|
||
}
|
||
|
||
async function batchAction(action) {
|
||
if (action === 'delete') {
|
||
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 个用户?`, '危险操作', { type: 'warning' })
|
||
}
|
||
await batchUserAction({ user_ids: selectedIds.value, action })
|
||
ElMessage.success('操作成功')
|
||
loadUsers()
|
||
}
|
||
|
||
async function toggleEnabled(row) {
|
||
await updateUser(row.id, { is_enabled: row.is_enabled ? 0 : 1 })
|
||
loadUsers()
|
||
}
|
||
|
||
async function doLogin(row) {
|
||
const loading = ElMessage({ message: `正在登录 ${row.nickname}...`, duration: 0 })
|
||
try {
|
||
await loginUser(row.id)
|
||
loading.close()
|
||
ElMessage.success('登录成功')
|
||
loadUsers()
|
||
} catch { loading.close() }
|
||
}
|
||
|
||
async function doLogout(row) {
|
||
await logoutUser(row.id)
|
||
ElMessage.success('已登出')
|
||
loadUsers()
|
||
}
|
||
|
||
function openPersonality(row) {
|
||
currentUser.value = row
|
||
currentPersonality.value = row.personality ? { ...row.personality } : null
|
||
personalityVisible.value = true
|
||
}
|
||
|
||
async function regenPersonality() {
|
||
regenLoading.value = true
|
||
try {
|
||
const res = await generatePersonality(currentUser.value.id)
|
||
currentPersonality.value = res.data
|
||
ElMessage.success('人格重新生成成功')
|
||
loadUsers()
|
||
} finally { regenLoading.value = false }
|
||
}
|
||
|
||
async function savePersonality() {
|
||
savingPersonality.value = true
|
||
try {
|
||
await updatePersonality(currentUser.value.id, currentPersonality.value)
|
||
ElMessage.success('人格已保存')
|
||
loadUsers()
|
||
} finally { savingPersonality.value = false }
|
||
}
|
||
|
||
async function downloadTemplate() {
|
||
const res = await dlTemplate()
|
||
const url = URL.createObjectURL(new Blob([res]))
|
||
const a = document.createElement('a'); a.href = url; a.download = 'import_template.xlsx'; a.click()
|
||
}
|
||
|
||
async function handleImport(file) {
|
||
const fd = new FormData(); fd.append('file', file)
|
||
// 显示进度提示
|
||
const loading = ElMessage({
|
||
message: '正在导入中,请稍候...',
|
||
type: 'info',
|
||
duration: 0,
|
||
showClose: false,
|
||
icon: 'Loading'
|
||
})
|
||
try {
|
||
const res = await importUsers(fd)
|
||
loading.close()
|
||
const d = res.data || {}
|
||
const success = d.success || 0
|
||
const failed = d.failed || 0
|
||
if (failed > 0 && success === 0) {
|
||
ElMessage.error(`导入失败:${failed}条失败,请检查数据格式`)
|
||
} else if (failed > 0) {
|
||
ElMessage.warning(`导入完成:成功 ${success} 条,失败 ${failed} 条`)
|
||
} else {
|
||
ElMessage.success(`导入成功:共导入 ${success} 条用户`)
|
||
}
|
||
// 导入完成后自动刷新列表
|
||
await loadUsers()
|
||
} catch(e) {
|
||
loading.close()
|
||
ElMessage.error('导入失败:' + (e.message || '未知错误'))
|
||
}
|
||
return false
|
||
}
|
||
|
||
async function handleExport() {
|
||
const res = await exportUsers()
|
||
const url = URL.createObjectURL(new Blob([res]))
|
||
const a = document.createElement('a'); a.href = url; a.download = 'users_export.xlsx'; a.click()
|
||
}
|
||
|
||
const loginAllLoading = ref(false)
|
||
const logoutAllLoading = ref(false)
|
||
|
||
const syncLoading = ref(false)
|
||
const avatarUploading = ref(false)
|
||
|
||
async function handleAvatarUpload(file) {
|
||
if (!editUser.value) return false
|
||
avatarUploading.value = true
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
formData.append('sync_to_platform', 'true') // 始终同步到平台
|
||
const res = await uploadAvatar(editUser.value.id, formData)
|
||
if (res.code === 200 || res.code === 0) {
|
||
form.avatar_url = res.data?.avatar_url || ''
|
||
ElMessage.success('头像上传成功')
|
||
} else {
|
||
ElMessage.error(res.message || '头像上传失败')
|
||
}
|
||
} catch(e) {
|
||
ElMessage.error('上传失败:' + (e.message || '未知错误'))
|
||
} finally {
|
||
avatarUploading.value = false
|
||
}
|
||
return false // 阻止 el-upload 默认上传
|
||
}
|
||
|
||
async function handleLogoutAll() {
|
||
logoutAllLoading.value = true
|
||
try {
|
||
const res = await request.post('/users/logout-all')
|
||
ElMessage.success(res.message || '已登出所有用户')
|
||
loadUsers()
|
||
} catch(e) {
|
||
ElMessage.error('登出失败:' + (e.message || '未知错误'))
|
||
} finally {
|
||
logoutAllLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSyncAll() {
|
||
syncLoading.value = true
|
||
try {
|
||
const res = await syncAllProfiles()
|
||
const d = res.data || {}
|
||
ElMessage.success(res.message || `同步完成:${d.synced} 个用户`)
|
||
await loadUsers()
|
||
} catch(e) {
|
||
ElMessage.error('同步失败:' + (e.message || '未知错误'))
|
||
} finally {
|
||
syncLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleLoginAll() {
|
||
loginAllLoading.value = true
|
||
try {
|
||
const res = await loginAllUsers()
|
||
const d = res.data || {}
|
||
ElMessage.success(res.message || `登录完成:成功 ${d.success} 个`)
|
||
await loadUsers()
|
||
} catch(e) {
|
||
ElMessage.error('一键登录失败:' + (e.message || '未知错误'))
|
||
} finally {
|
||
loginAllLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleDeduplicate() {
|
||
const res = await deduplicateUsers()
|
||
ElMessage.success(res.message || '去重完成')
|
||
loadUsers()
|
||
}
|
||
|
||
// 暗色主题行样式:深浅交替,避免白底
|
||
function rowClassName({ rowIndex }) {
|
||
return rowIndex % 2 === 0 ? 'row-dark' : 'row-darker'
|
||
}
|
||
|
||
onMounted(() => { loadUsers(); window.addEventListener('page-refresh', loadUsers) })
|
||
onUnmounted(() => window.removeEventListener('page-refresh', loadUsers))
|
||
</script>
|
||
|
||
<style scoped>
|
||
.filter-bar { display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }
|
||
.batch-bar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.3); border-radius: 8px; margin-bottom: 10px; }
|
||
.batch-info { font-size: 13px; color: var(--color-accent); margin-right: 4px; }
|
||
.data-table { border-radius: 8px; overflow: hidden; }
|
||
.personality-panel { }
|
||
.p-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||
.p-user { font-size: 15px; font-weight: 600; }
|
||
.p-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.p-item { display: flex; align-items: center; gap: 10px; }
|
||
.p-key { font-size: 12px; color: var(--color-text-muted); white-space: nowrap; width: 60px; flex-shrink: 0; }
|
||
.p-empty { padding: 20px 0; }
|
||
|
||
/* 暗色主题表格行样式 */
|
||
: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>
|