2026-04-03 16:32:59 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<!-- <ContentWrap class="page-hero">
|
|
|
|
|
|
<div class="hero-panel">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="hero-eyebrow">AI Companion Admin</div>
|
|
|
|
|
|
<div class="hero-title">陪聊角色管理</div>
|
|
|
|
|
|
<div class="hero-subtitle">
|
|
|
|
|
|
聚焦角色筛选、上线状态和人设浏览,降低长表格的阅读负担。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="hero-metrics">
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<div class="metric-label">当前页角色</div>
|
|
|
|
|
|
<div class="metric-value">{{ list.length }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<div class="metric-label">在线角色</div>
|
|
|
|
|
|
<div class="metric-value">{{ onlineCount }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card">
|
|
|
|
|
|
<div class="metric-label">公开角色</div>
|
|
|
|
|
|
<div class="metric-value">{{ publicCount }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-card accent">
|
|
|
|
|
|
<div class="metric-label">已选中</div>
|
|
|
|
|
|
<div class="metric-value">{{ checkedIds.length }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ContentWrap> -->
|
|
|
|
|
|
|
2026-02-10 15:20:30 +08:00
|
|
|
|
<ContentWrap>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<div class="toolbar-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="section-title">筛选条件</div>
|
|
|
|
|
|
<div class="section-desc">保留高频条件直出,低频条件收进高级筛选。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button text type="primary" @click="showAdvancedFilters = !showAdvancedFilters">
|
|
|
|
|
|
<Icon :icon="showAdvancedFilters ? 'ep:arrow-up' : 'ep:arrow-down'" class="mr-4px" />
|
|
|
|
|
|
{{ showAdvancedFilters ? '收起高级筛选' : '展开高级筛选' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form ref="queryFormRef" :model="queryParams" label-width="90px" class="filter-form">
|
|
|
|
|
|
<div class="filter-grid">
|
|
|
|
|
|
<el-form-item label="性别" prop="gender">
|
|
|
|
|
|
<el-select v-model="queryParams.gender" placeholder="全部性别" clearable filterable class="!w-full">
|
|
|
|
|
|
<el-option label="Male" value="male" />
|
|
|
|
|
|
<el-option label="Female" value="female" />
|
|
|
|
|
|
<el-option label="Other" value="other" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
|
|
|
<el-select v-model="queryParams.status" placeholder="全部状态" clearable class="!w-full">
|
|
|
|
|
|
<el-option label="在线" :value="1" />
|
|
|
|
|
|
<el-option label="离线" :value="0" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="可见性" prop="visibility">
|
|
|
|
|
|
<el-select v-model="queryParams.visibility" placeholder="全部可见性" clearable class="!w-full">
|
|
|
|
|
|
<el-option label="公开" :value="1" />
|
|
|
|
|
|
<el-option label="内测" :value="2" />
|
|
|
|
|
|
<el-option label="隐藏" :value="3" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="性格标签" prop="personalityTags">
|
|
|
|
|
|
<el-input v-model="queryParams.personalityTags" placeholder="如:温柔、治愈" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="说话风格" prop="speakingStyle">
|
|
|
|
|
|
<el-input v-model="queryParams.speakingStyle" placeholder="如:理性、活泼" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="语音 ID" prop="voiceId">
|
|
|
|
|
|
<el-input v-model="queryParams.voiceId" placeholder="请输入 voiceId" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-collapse-transition>
|
|
|
|
|
|
<div v-show="showAdvancedFilters" class="advanced-panel">
|
|
|
|
|
|
<div class="filter-grid">
|
|
|
|
|
|
<el-form-item label="年龄段" prop="ageRange">
|
|
|
|
|
|
<el-input v-model="queryParams.ageRange" placeholder="如:20s、25-30" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="系统 Prompt" prop="systemPrompt">
|
|
|
|
|
|
<el-input v-model="queryParams.systemPrompt" placeholder="按核心人设搜索" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="开场白" prop="prologue">
|
|
|
|
|
|
<el-input v-model="queryParams.prologue" placeholder="搜索开场白内容" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="热度分" prop="popularityScore">
|
|
|
|
|
|
<el-input v-model="queryParams.popularityScore" placeholder="按热度分搜索" clearable
|
|
|
|
|
|
@keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="排序权重" prop="sortOrder">
|
|
|
|
|
|
<el-input v-model="queryParams.sortOrder" placeholder="按排序权重搜索" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="创建时间" prop="createdAt">
|
|
|
|
|
|
<el-date-picker v-model="queryParams.createdAt" type="date" value-format="YYYY-MM-DD" placeholder="选择创建日期"
|
|
|
|
|
|
clearable class="!w-full" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="更新时间" prop="updatedAt">
|
|
|
|
|
|
<el-date-picker v-model="queryParams.updatedAt" type="date" value-format="YYYY-MM-DD" placeholder="选择更新日期"
|
|
|
|
|
|
clearable class="!w-full" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="头像 URL" prop="avatarUrl">
|
|
|
|
|
|
<el-input v-model="queryParams.avatarUrl" placeholder="匹配头像 URL" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="封面 URL" prop="coverImageUrl">
|
|
|
|
|
|
<el-input v-model="queryParams.coverImageUrl" placeholder="匹配封面 URL" clearable
|
|
|
|
|
|
@keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="开场白音频" prop="prologueAudio">
|
|
|
|
|
|
<el-input v-model="queryParams.prologueAudio" placeholder="匹配音频地址" clearable @keyup.enter="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-collapse-transition>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="toolbar-actions">
|
|
|
|
|
|
<div class="action-group">
|
|
|
|
|
|
<el-button @click="handleQuery">
|
|
|
|
|
|
<Icon icon="ep:search" class="mr-5px" />
|
|
|
|
|
|
搜索
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button @click="resetQuery">
|
|
|
|
|
|
<Icon icon="ep:refresh" class="mr-5px" />
|
|
|
|
|
|
重置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-group">
|
|
|
|
|
|
<el-button type="primary" @click="openForm('create')" v-hasPermi="['keyboard:ai-companion:create']">
|
|
|
|
|
|
<Icon icon="ep:plus" class="mr-5px" />
|
|
|
|
|
|
新增角色
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
|
|
|
|
|
|
v-hasPermi="['keyboard:ai-companion:export']">
|
|
|
|
|
|
<Icon icon="ep:download" class="mr-5px" />
|
|
|
|
|
|
导出
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
|
|
|
|
|
|
v-hasPermi="['keyboard:ai-companion:delete']">
|
|
|
|
|
|
<Icon icon="ep:delete" class="mr-5px" />
|
|
|
|
|
|
批量删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-10 15:20:30 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
</ContentWrap>
|
|
|
|
|
|
|
|
|
|
|
|
<ContentWrap>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<div class="toolbar-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="section-title">角色列表</div>
|
|
|
|
|
|
<div class="section-desc">
|
|
|
|
|
|
共 {{ total }} 条记录,当前展示 {{ list.length }} 条。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="selection-indicator" :class="{ active: checkedIds.length > 0 }">
|
|
|
|
|
|
<Icon icon="ep:select" class="mr-6px" />
|
|
|
|
|
|
已勾选 {{ checkedIds.length }} 项
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table row-key="id" v-loading="loading" :data="list" stripe highlight-current-row :show-overflow-tooltip="false"
|
|
|
|
|
|
@current-change="handleCurrentChange" @selection-change="handleRowCheckboxChange">
|
|
|
|
|
|
<el-table-column type="selection" width="52" />
|
|
|
|
|
|
<el-table-column label="角色信息" min-width="300">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="companion-cell">
|
|
|
|
|
|
<el-image v-if="row.coverImageUrl" :src="row.coverImageUrl" :preview-src-list="buildPreviewList(row)"
|
|
|
|
|
|
fit="cover" class="cover-image" preview-teleported />
|
|
|
|
|
|
<div v-else class="cover-image cover-image--empty">无封面</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="companion-main">
|
|
|
|
|
|
<div class="companion-head">
|
|
|
|
|
|
<el-avatar v-if="row.avatarUrl" :src="row.avatarUrl" :size="42" />
|
|
|
|
|
|
<div v-else class="avatar-fallback">AI</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="companion-id">ID {{ row.id }}</div>
|
|
|
|
|
|
<div class="companion-meta">
|
|
|
|
|
|
<span>{{ formatGender(row.gender) }}</span>
|
|
|
|
|
|
<span>{{ row.ageRange || '年龄未填' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="tag-row">
|
|
|
|
|
|
<el-tag v-for="tag in getTagList(row.personalityTags)" :key="tag" size="small" effect="plain" round>
|
|
|
|
|
|
{{ tag }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span v-if="getTagList(row.personalityTags).length === 0" class="muted-text">
|
|
|
|
|
|
暂无标签
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-10 15:20:30 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<el-table-column label="人设概览" min-width="330">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="stack-text">
|
|
|
|
|
|
<div class="stack-title">{{ row.speakingStyle || '未填写说话风格' }}</div>
|
|
|
|
|
|
<div class="stack-desc line-clamp-2">
|
|
|
|
|
|
{{ row.systemPrompt || '暂无系统 Prompt' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-10 15:20:30 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<el-table-column label="状态" width="170" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="status-stack">
|
|
|
|
|
|
<el-tag :type="getStatusMeta(row.status).type" round>
|
|
|
|
|
|
{{ getStatusMeta(row.status).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<el-tag :type="getVisibilityMeta(row.visibility).type" effect="plain" round>
|
|
|
|
|
|
{{ getVisibilityMeta(row.visibility).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="排序 / 热度" width="150" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="score-stack">
|
|
|
|
|
|
<div>排序 {{ row.sortOrder ?? '-' }}</div>
|
|
|
|
|
|
<div class="score-highlight">热度 {{ row.popularityScore ?? '-' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="开场白" min-width="260">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="line-clamp-3">{{ row.prologue || '暂无开场白' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="语音" min-width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="stack-text">
|
|
|
|
|
|
<div class="stack-title mono-text">{{ row.voiceId || '-' }}</div>
|
|
|
|
|
|
<div class="stack-desc line-clamp-1">{{ row.prologueAudio || '未配置开场音频' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="更新时间" width="180" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ dateFormatter(row.updatedAt) || '-' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" align="center" width="130" fixed="right">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-button link type="primary" @click="openForm('update', row.id)"
|
2026-04-03 16:04:02 +08:00
|
|
|
|
v-hasPermi="['keyboard:ai-companion:update']">
|
2026-02-10 15:20:30 +08:00
|
|
|
|
编辑
|
|
|
|
|
|
</el-button>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<el-button link type="danger" @click="handleDelete(row.id)" v-hasPermi="['keyboard:ai-companion:delete']">
|
2026-02-10 15:20:30 +08:00
|
|
|
|
删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
|
2026-04-03 16:04:02 +08:00
|
|
|
|
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
|
|
|
|
|
|
@pagination="getList" />
|
2026-02-10 15:20:30 +08:00
|
|
|
|
</ContentWrap>
|
|
|
|
|
|
|
2026-04-03 16:04:02 +08:00
|
|
|
|
<ContentWrap>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
<div class="toolbar-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="section-title">角色详情</div>
|
|
|
|
|
|
<div class="section-desc">点击表格行查看当前角色的人设摘要和国际化内容。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="currentRow.id" class="selection-indicator active">
|
|
|
|
|
|
当前查看 ID {{ currentRow.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-empty v-if="!currentRow.id" description="请选择上方列表中的一条角色记录" :image-size="120" />
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<div class="detail-banner">
|
|
|
|
|
|
<el-avatar v-if="currentRow.avatarUrl" :src="currentRow.avatarUrl" :size="56" />
|
|
|
|
|
|
<div v-else class="avatar-fallback avatar-fallback--large">AI</div>
|
|
|
|
|
|
<div class="detail-banner__content">
|
|
|
|
|
|
<div class="detail-banner__title">
|
|
|
|
|
|
角色 #{{ currentRow.id }}
|
|
|
|
|
|
<el-tag size="small" :type="getStatusMeta(currentRow.status).type" round>
|
|
|
|
|
|
{{ getStatusMeta(currentRow.status).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<el-tag size="small" :type="getVisibilityMeta(currentRow.visibility).type" effect="plain" round>
|
|
|
|
|
|
{{ getVisibilityMeta(currentRow.visibility).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="detail-banner__subtitle">
|
|
|
|
|
|
{{ formatGender(currentRow.gender) }} / {{ currentRow.ageRange || '年龄未填' }} / {{
|
|
|
|
|
|
currentRow.speakingStyle || '说话风格未填'
|
|
|
|
|
|
}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
|
<el-col :xs="24" :lg="14">
|
|
|
|
|
|
<el-card shadow="never" class="detail-card">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="detail-card__title">角色摘要</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-descriptions :column="1" border>
|
|
|
|
|
|
<el-descriptions-item label="性格标签">
|
|
|
|
|
|
<div class="tag-row">
|
|
|
|
|
|
<el-tag v-for="tag in getTagList(currentRow.personalityTags)" :key="tag" size="small" effect="plain"
|
|
|
|
|
|
round>
|
|
|
|
|
|
{{ tag }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span v-if="getTagList(currentRow.personalityTags).length === 0">暂无标签</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="系统 Prompt">
|
|
|
|
|
|
{{ currentRow.systemPrompt || '暂无系统 Prompt' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="开场白">
|
|
|
|
|
|
{{ currentRow.prologue || '暂无开场白' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="语音配置">
|
|
|
|
|
|
<div class="detail-audio">
|
|
|
|
|
|
<span>voiceId:{{ currentRow.voiceId || '-' }}</span>
|
|
|
|
|
|
<span>音频:{{ currentRow.prologueAudio || '未配置' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :xs="24" :lg="10">
|
|
|
|
|
|
<el-card shadow="never" class="detail-card">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="detail-card__title">运营信息</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-descriptions :column="1" border>
|
|
|
|
|
|
<el-descriptions-item label="排序权重">
|
|
|
|
|
|
{{ currentRow.sortOrder ?? '-' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="热度评分">
|
|
|
|
|
|
{{ currentRow.popularityScore ?? '-' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="创建时间">
|
|
|
|
|
|
{{ dateFormatter(currentRow.createdAt) || '-' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="更新时间">
|
|
|
|
|
|
{{ dateFormatter(currentRow.updatedAt) || '-' }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="i18n-panel">
|
|
|
|
|
|
<el-tabs model-value="aiCompanionI18n">
|
|
|
|
|
|
<el-tab-pane label="国际化内容" name="aiCompanionI18n">
|
|
|
|
|
|
<AiCompanionI18nList :id="currentRow.id" />
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-04-03 16:04:02 +08:00
|
|
|
|
</ContentWrap>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
<AiCompanionForm ref="formRef" @success="getList" />
|
2026-02-10 15:20:30 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { dateFormatter } from '@/utils/formatTime'
|
|
|
|
|
|
import download from '@/utils/download'
|
2026-04-03 16:32:59 +08:00
|
|
|
|
import { isEmpty } from '@/utils/is'
|
|
|
|
|
|
import { AiCompanionApi, type AiCompanion } from '@/api/keyboard/aicompanion'
|
2026-02-10 15:20:30 +08:00
|
|
|
|
import AiCompanionForm from './AiCompanionForm.vue'
|
2026-04-03 16:04:02 +08:00
|
|
|
|
import AiCompanionI18nList from './components/AiCompanionI18nList.vue'
|
2026-02-10 15:20:30 +08:00
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'KeyboardAiCompanion' })
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
type QueryParams = {
|
|
|
|
|
|
pageNo: number
|
|
|
|
|
|
pageSize: number
|
|
|
|
|
|
avatarUrl: string | undefined
|
|
|
|
|
|
coverImageUrl: string | undefined
|
|
|
|
|
|
gender: string | undefined
|
|
|
|
|
|
ageRange: string | undefined
|
|
|
|
|
|
personalityTags: string | undefined
|
|
|
|
|
|
speakingStyle: string | undefined
|
|
|
|
|
|
systemPrompt: string | undefined
|
|
|
|
|
|
status: number | undefined
|
|
|
|
|
|
visibility: number | undefined
|
|
|
|
|
|
sortOrder: number | string | undefined
|
|
|
|
|
|
popularityScore: number | string | undefined
|
|
|
|
|
|
createdAt: string | undefined
|
|
|
|
|
|
updatedAt: string | undefined
|
|
|
|
|
|
prologue: string | undefined
|
|
|
|
|
|
prologueAudio: string | undefined
|
|
|
|
|
|
voiceId: string | undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const message = useMessage()
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const exportLoading = ref(false)
|
|
|
|
|
|
const showAdvancedFilters = ref(false)
|
|
|
|
|
|
const list = ref<AiCompanion[]>([])
|
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
|
const checkedIds = ref<number[]>([])
|
|
|
|
|
|
const currentRow = ref<Partial<AiCompanion>>({})
|
|
|
|
|
|
const queryFormRef = ref()
|
|
|
|
|
|
const formRef = ref()
|
2026-02-10 15:20:30 +08:00
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
const queryParams = reactive<QueryParams>({
|
2026-02-10 15:20:30 +08:00
|
|
|
|
pageNo: 1,
|
|
|
|
|
|
pageSize: 10,
|
2026-04-03 16:04:02 +08:00
|
|
|
|
avatarUrl: undefined,
|
|
|
|
|
|
coverImageUrl: undefined,
|
2026-02-10 15:20:30 +08:00
|
|
|
|
gender: undefined,
|
2026-04-03 16:04:02 +08:00
|
|
|
|
ageRange: undefined,
|
|
|
|
|
|
personalityTags: undefined,
|
|
|
|
|
|
speakingStyle: undefined,
|
|
|
|
|
|
systemPrompt: undefined,
|
2026-02-10 15:20:30 +08:00
|
|
|
|
status: undefined,
|
|
|
|
|
|
visibility: undefined,
|
2026-04-03 16:04:02 +08:00
|
|
|
|
sortOrder: undefined,
|
|
|
|
|
|
popularityScore: undefined,
|
|
|
|
|
|
createdAt: undefined,
|
|
|
|
|
|
updatedAt: undefined,
|
|
|
|
|
|
prologue: undefined,
|
|
|
|
|
|
prologueAudio: undefined,
|
|
|
|
|
|
voiceId: undefined
|
2026-02-10 15:20:30 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
const onlineCount = computed(() => list.value.filter((item) => Number(item.status) === 1).length)
|
|
|
|
|
|
const publicCount = computed(() => list.value.filter((item) => Number(item.visibility) === 1).length)
|
|
|
|
|
|
|
2026-02-10 15:20:30 +08:00
|
|
|
|
const getList = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await AiCompanionApi.getAiCompanionPage(queryParams)
|
2026-04-03 16:32:59 +08:00
|
|
|
|
list.value = data.list || []
|
|
|
|
|
|
total.value = data.total || 0
|
|
|
|
|
|
syncCurrentRow()
|
2026-02-10 15:20:30 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
const syncCurrentRow = () => {
|
|
|
|
|
|
if (!list.value.length) {
|
|
|
|
|
|
currentRow.value = {}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const matchedRow = list.value.find((item) => item.id === currentRow.value.id)
|
|
|
|
|
|
currentRow.value = matchedRow || list.value[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 15:20:30 +08:00
|
|
|
|
const handleQuery = () => {
|
|
|
|
|
|
queryParams.pageNo = 1
|
|
|
|
|
|
getList()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetQuery = () => {
|
2026-04-03 16:32:59 +08:00
|
|
|
|
queryFormRef.value?.resetFields()
|
|
|
|
|
|
checkedIds.value = []
|
2026-02-10 15:20:30 +08:00
|
|
|
|
handleQuery()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openForm = (type: string, id?: number) => {
|
2026-04-03 16:32:59 +08:00
|
|
|
|
formRef.value?.open(type, id)
|
2026-02-10 15:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: number) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await message.delConfirm()
|
|
|
|
|
|
await AiCompanionApi.deleteAiCompanion(id)
|
|
|
|
|
|
message.success(t('common.delSuccess'))
|
2026-04-03 16:32:59 +08:00
|
|
|
|
checkedIds.value = checkedIds.value.filter((item) => item !== id)
|
|
|
|
|
|
if (currentRow.value.id === id) {
|
|
|
|
|
|
currentRow.value = {}
|
|
|
|
|
|
}
|
2026-02-10 15:20:30 +08:00
|
|
|
|
await getList()
|
2026-04-03 16:04:02 +08:00
|
|
|
|
} catch { }
|
2026-02-10 15:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteBatch = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await message.delConfirm()
|
2026-04-03 16:32:59 +08:00
|
|
|
|
await AiCompanionApi.deleteAiCompanionList(checkedIds.value)
|
|
|
|
|
|
checkedIds.value = []
|
2026-02-10 15:20:30 +08:00
|
|
|
|
message.success(t('common.delSuccess'))
|
2026-04-03 16:32:59 +08:00
|
|
|
|
await getList()
|
2026-04-03 16:04:02 +08:00
|
|
|
|
} catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRowCheckboxChange = (records: AiCompanion[]) => {
|
2026-04-03 16:32:59 +08:00
|
|
|
|
checkedIds.value = records.map((item) => item.id)
|
2026-02-10 15:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleExport = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await message.exportConfirm()
|
|
|
|
|
|
exportLoading.value = true
|
|
|
|
|
|
const data = await AiCompanionApi.exportAiCompanion(queryParams)
|
2026-04-03 16:32:59 +08:00
|
|
|
|
download.excel(data, 'AI陪聊角色.xls')
|
2026-02-10 15:20:30 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
exportLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
const handleCurrentChange = (row?: AiCompanion) => {
|
|
|
|
|
|
currentRow.value = row || {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusMeta = (status?: number) => {
|
|
|
|
|
|
if (Number(status) === 1) {
|
|
|
|
|
|
return { label: '在线', type: 'success' as const }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { label: '离线', type: 'info' as const }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getVisibilityMeta = (visibility?: number) => {
|
|
|
|
|
|
if (Number(visibility) === 1) {
|
|
|
|
|
|
return { label: '公开', type: 'success' as const }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Number(visibility) === 2) {
|
|
|
|
|
|
return { label: '内测', type: 'warning' as const }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { label: '隐藏', type: 'info' as const }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatGender = (gender?: string) => {
|
|
|
|
|
|
if (!gender) return '未设置性别'
|
|
|
|
|
|
const normalized = gender.toLowerCase()
|
|
|
|
|
|
if (normalized === 'male') return 'Male'
|
|
|
|
|
|
if (normalized === 'female') return 'Female'
|
|
|
|
|
|
if (normalized === 'other') return 'Other'
|
|
|
|
|
|
return gender
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getTagList = (tags?: AiCompanion['personalityTags']) => {
|
|
|
|
|
|
if (!tags) return []
|
|
|
|
|
|
if (Array.isArray(tags)) {
|
|
|
|
|
|
return tags.map((item) => String(item)).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof tags === 'string') {
|
|
|
|
|
|
return tags
|
|
|
|
|
|
.split(/[,,、]/)
|
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const buildPreviewList = (row: AiCompanion) => {
|
|
|
|
|
|
return [row.coverImageUrl, row.avatarUrl].filter(Boolean) as string[]
|
2026-04-03 16:04:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 15:20:30 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
getList()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
2026-04-03 16:04:02 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-04-03 16:32:59 +08:00
|
|
|
|
.page-hero {
|
|
|
|
|
|
overflow: hidden;
|
2026-04-03 16:04:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
.hero-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
color: #163247;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-eyebrow {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
color: #5b7a90;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.12em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-title {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-subtitle {
|
|
|
|
|
|
max-width: 640px;
|
|
|
|
|
|
color: #5f7386;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-metrics {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-card {
|
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: linear-gradient(180deg, rgb(255 255 255 / 90%), rgb(243 248 252 / 92%));
|
|
|
|
|
|
border: 1px solid rgb(118 153 177 / 18%);
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
box-shadow: 0 12px 30px rgb(109 141 160 / 10%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-card.accent {
|
|
|
|
|
|
background: linear-gradient(135deg, #1c7ed6, #34a0a4);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-label {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-value {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-desc {
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-form :deep(.el-form-item) {
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
|
|
|
|
gap: 0 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.advanced-panel {
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
border-top: 1px dashed var(--el-border-color-lighter);
|
2026-04-03 16:04:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
.toolbar-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-indicator {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
transition: all var(--el-transition-duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selection-indicator.active {
|
|
|
|
|
|
color: #155e75;
|
|
|
|
|
|
background: #e0f2fe;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.companion-cell {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.companion-main {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.companion-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.companion-id {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-weight: 700;
|
2026-04-03 16:04:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
.companion-meta {
|
2026-04-03 16:04:02 +08:00
|
|
|
|
display: flex;
|
2026-04-03 16:32:59 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 3px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-fallback {
|
|
|
|
|
|
display: inline-flex;
|
2026-04-03 16:04:02 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-03 16:32:59 +08:00
|
|
|
|
width: 42px;
|
|
|
|
|
|
height: 42px;
|
|
|
|
|
|
color: #155e75;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
background: linear-gradient(135deg, #d7f3ff, #eefaf5);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.avatar-fallback--large {
|
|
|
|
|
|
width: 56px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cover-image {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 88px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cover-image--empty {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.muted-text {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
2026-04-03 16:04:02 +08:00
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 16:32:59 +08:00
|
|
|
|
.stack-text {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stack-title {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stack-desc {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-stack,
|
|
|
|
|
|
.score-stack {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.score-highlight {
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mono-text {
|
|
|
|
|
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-banner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 18px 20px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
background: linear-gradient(135deg, #f7fbff, #f7fcfa);
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-banner__content {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-banner__title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-banner__subtitle {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-card__title {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-audio {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.i18n-panel {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table .cell) {
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table__row.current-row > td.el-table__cell) {
|
|
|
|
|
|
background: #f0f9ff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-image__error),
|
2026-04-03 16:04:02 +08:00
|
|
|
|
:deep(.el-image__placeholder) {
|
2026-04-03 16:32:59 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 992px) {
|
|
|
|
|
|
|
|
|
|
|
|
.hero-panel,
|
|
|
|
|
|
.toolbar-header,
|
|
|
|
|
|
.toolbar-actions {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-metrics {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
|
.hero-title {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-metrics {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.companion-cell,
|
|
|
|
|
|
.detail-banner {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
2026-04-03 16:04:02 +08:00
|
|
|
|
}
|
2026-04-03 16:32:59 +08:00
|
|
|
|
</style>
|