v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化
This commit is contained in:
@@ -19,8 +19,8 @@ const apiClient: AxiosInstance = axios.create({
|
||||
// 基础 URL:所有请求会自动加上这个前缀
|
||||
// 开发环境通过 Vite proxy 转发到后端
|
||||
baseURL: '/api',
|
||||
// 请求超时时间(10秒)
|
||||
timeout: 10000,
|
||||
// 请求超时时间(30秒,联软API测试等操作可能较慢)
|
||||
timeout: 30000,
|
||||
// 默认请求头
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 欢迎与引导配置 API
|
||||
// =============================================================================
|
||||
// 说明:封装欢迎页、引导视频、互动教学、主题模板的 API 调用
|
||||
|
||||
import apiClient from './index'
|
||||
|
||||
// ==========================================================================
|
||||
// 类型定义
|
||||
// ==========================================================================
|
||||
|
||||
/** 欢迎页配置 */
|
||||
export interface WelcomePageConfig {
|
||||
enabled: boolean
|
||||
title: string
|
||||
content: string
|
||||
background_color: string
|
||||
}
|
||||
|
||||
/** 引导视频配置 */
|
||||
export interface WelcomeVideoConfig {
|
||||
url: string
|
||||
filename: string
|
||||
uploaded_at: string
|
||||
}
|
||||
|
||||
/** 互动教学步骤 */
|
||||
export interface TutorialStep {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/** 互动教学配置 */
|
||||
export interface TutorialConfig {
|
||||
enabled: boolean
|
||||
steps: TutorialStep[]
|
||||
}
|
||||
|
||||
/** 主题模板配置 */
|
||||
export interface ThemeConfig {
|
||||
theme: 'default' | 'blue' | 'orange' | 'red'
|
||||
}
|
||||
|
||||
/** 完整欢迎与引导配置 */
|
||||
export interface WelcomeConfig {
|
||||
welcome_page: WelcomePageConfig
|
||||
welcome_video: WelcomeVideoConfig | null
|
||||
tutorial: TutorialConfig
|
||||
theme: ThemeConfig
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// API 函数
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 获取欢迎与引导配置
|
||||
*/
|
||||
export function getWelcomeConfig(): Promise<{ data: { code: number; data: WelcomeConfig; message: string } }> {
|
||||
return apiClient.get('/admin/welcome-config')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新欢迎与引导配置
|
||||
*/
|
||||
export function updateWelcomeConfig(
|
||||
data: WelcomeConfig
|
||||
): Promise<{ data: { code: number; data: WelcomeConfig; message: string } }> {
|
||||
return apiClient.put('/admin/welcome-config', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传引导视频
|
||||
*/
|
||||
export function uploadWelcomeVideo(
|
||||
file: File
|
||||
): Promise<{ data: { code: number; data: { url: string; filename: string; uploaded_at: string }; message: string } }> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return apiClient.post('/admin/welcome-video', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除引导视频
|
||||
*/
|
||||
export function deleteWelcomeVideo(): Promise<{ data: { code: number; data: null; message: string } }> {
|
||||
return apiClient.delete('/admin/welcome-video')
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 预设主题颜色映射
|
||||
// ==========================================================================
|
||||
|
||||
export const THEME_COLORS: Record<string, string> = {
|
||||
default: '#07C160', // 绿色
|
||||
blue: '#1989FA', // 蓝色
|
||||
orange: '#E6A23C', // 橙色
|
||||
red: '#F56C6C', // 红色
|
||||
}
|
||||
|
||||
export const THEME_LABELS: Record<string, string> = {
|
||||
default: '默认绿色',
|
||||
blue: '商务蓝色',
|
||||
orange: '活力橙色',
|
||||
red: '热情红色',
|
||||
}
|
||||
@@ -111,7 +111,7 @@ async function handleSearch(): Promise<void> {
|
||||
|
||||
try {
|
||||
const response = await globalSearch(query)
|
||||
results.value = response.data.data.items
|
||||
results.value = response.items
|
||||
} catch {
|
||||
results.value = []
|
||||
} finally {
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
<el-icon><Share /></el-icon>
|
||||
<span>排查流程图</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/welcome-config">
|
||||
<el-icon><Guide /></el-icon>
|
||||
<span>欢迎与引导</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 📊 监控与数据 -->
|
||||
<div class="menu-section-title">📊 监控与数据</div>
|
||||
@@ -177,7 +181,7 @@
|
||||
// ==========================================================================
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook, Star, MagicStick, Files, Upload, Filter } from '@element-plus/icons-vue'
|
||||
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook, Star, MagicStick, Files, Upload, Filter, Guide } from '@element-plus/icons-vue'
|
||||
|
||||
// ==========================================================================
|
||||
// 当前激活菜单
|
||||
|
||||
@@ -140,7 +140,14 @@ const routes = [
|
||||
meta: { title: '运行期日志', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
// P2 占位页:主题模板
|
||||
// 欢迎与引导配置
|
||||
path: 'welcome-config',
|
||||
name: 'WelcomeConfig',
|
||||
component: () => import('@/views/WelcomeConfig.vue'),
|
||||
meta: { title: '欢迎与引导', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
// P2 占位页:主题模板(已整合到欢迎与引导配置)
|
||||
path: 'themes',
|
||||
name: 'Themes',
|
||||
component: () => import('@/views/Placeholder.vue'),
|
||||
|
||||
@@ -19,6 +19,7 @@ import router from '@/router'
|
||||
// --------------------------------------------------------------------------
|
||||
const TOKEN_KEY = 'admin_token'
|
||||
const ADMIN_USER_ID_KEY = 'admin_user_id'
|
||||
const ADMIN_INFO_KEY = 'admin_info'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
@@ -28,8 +29,9 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 当前管理员信息 */
|
||||
const adminInfo = ref<Agent | null>(null)
|
||||
/** 当前管理员信息(尝试从 localStorage 恢复) */
|
||||
const storedInfo = localStorage.getItem(ADMIN_INFO_KEY)
|
||||
const adminInfo = ref<Agent | null>(storedInfo ? JSON.parse(storedInfo) : null)
|
||||
|
||||
/** 认证 token */
|
||||
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||
@@ -96,11 +98,13 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
localStorage.setItem(TOKEN_KEY, data.token || agentInfoData.token)
|
||||
localStorage.setItem(ADMIN_USER_ID_KEY, inputUserId)
|
||||
|
||||
// 保存管理员信息
|
||||
adminInfo.value = {
|
||||
// 保存管理员信息(持久化到 localStorage)
|
||||
const adminInfoData = {
|
||||
...agentInfoData,
|
||||
role: 'admin',
|
||||
} as Agent
|
||||
adminInfo.value = adminInfoData
|
||||
localStorage.setItem(ADMIN_INFO_KEY, JSON.stringify(adminInfoData))
|
||||
|
||||
// 跳转到仪表盘
|
||||
router.push('/dashboard')
|
||||
@@ -132,6 +136,7 @@ export const useAdminStore = defineStore('admin', () => {
|
||||
// 清除 localStorage
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
||||
localStorage.removeItem(ADMIN_INFO_KEY)
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
|
||||
@@ -68,7 +68,7 @@ export const useAgentStore = defineStore('agent', () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await apiGetAgents(status)
|
||||
agents.value = response.data.data.items
|
||||
agents.value = response.items
|
||||
} catch (error) {
|
||||
console.error('加载坐席列表失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 企微IT智能服务台 — 阶段5 自动化闭环 状态管理(Pinia Store,管理后台)
|
||||
// =============================================================================
|
||||
// 说明:管理自动化场景配置、规则版本、运营指标。
|
||||
// 约定:api 返回包装对象,调用方统一从 res.data.data 取业务数据。
|
||||
// 约定:响应拦截器已剥离 axios 外壳与 {code,data,message} 信封,调用方直接使用返回值(即 data 字段本身)。
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -39,7 +39,7 @@ export const useAutomationStore = defineStore('automation', () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAutomationConfigs()
|
||||
scenarios.value = res.data.data
|
||||
scenarios.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export const useAutomationStore = defineStore('automation', () => {
|
||||
payload: UpdateScenarioPayload,
|
||||
): Promise<ScenarioConfig> {
|
||||
const res = await updateAutomationConfig(id, payload)
|
||||
const updated = res.data.data
|
||||
const updated = res
|
||||
// 本地同步,避免整表刷新
|
||||
const idx = scenarios.value.findIndex((s) => s.id === id)
|
||||
if (idx >= 0) scenarios.value[idx] = updated
|
||||
@@ -63,7 +63,7 @@ export const useAutomationStore = defineStore('automation', () => {
|
||||
async function loadVersions(scenarioKey: string): Promise<void> {
|
||||
currentScenarioKey.value = scenarioKey
|
||||
const res = await getConfigVersions(scenarioKey)
|
||||
versions.value = res.data.data
|
||||
versions.value = res
|
||||
}
|
||||
|
||||
async function publishVersion(
|
||||
@@ -71,7 +71,7 @@ export const useAutomationStore = defineStore('automation', () => {
|
||||
payload: PublishVersionPayload,
|
||||
): Promise<RuleVersion> {
|
||||
const res = await publishConfigVersion(id, payload)
|
||||
const created = res.data.data
|
||||
const created = res
|
||||
versions.value.unshift(created)
|
||||
return created
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export const useAutomationStore = defineStore('automation', () => {
|
||||
// ------------------------------------------------------------------------
|
||||
async function loadMetrics(): Promise<void> {
|
||||
const res = await getAutomationMetrics()
|
||||
metrics.value = res.data.data
|
||||
metrics.value = res
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getConfigGroups()
|
||||
groups.value = response.data.data.groups
|
||||
groups.value = response.groups
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
} finally {
|
||||
@@ -68,8 +68,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
*/
|
||||
async function updateConfigValue(key: string, value: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await apiUpdateConfig(key, value)
|
||||
const result = response.data.data
|
||||
await apiUpdateConfig(key, value)
|
||||
|
||||
// 更新本地缓存中的值
|
||||
for (const group of groups.value) {
|
||||
@@ -96,7 +95,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
historyKey.value = key
|
||||
try {
|
||||
const response = await getConfigHistory(key, limit)
|
||||
historyItems.value = response.data.data.items
|
||||
historyItems.value = response.items
|
||||
} catch (error) {
|
||||
console.error('加载变更历史失败:', error)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const useKnowledgeStore = defineStore('knowledge', () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getKnowledgeBase(category, searchKeyword)
|
||||
items.value = response.data.data.items
|
||||
items.value = response.items
|
||||
} catch (error) {
|
||||
console.error('加载知识库列表失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const useQuickReplyStore = defineStore('quickReply', () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getPendingQuickReplies(category)
|
||||
replies.value = response.data.data.items
|
||||
replies.value = response.items
|
||||
} catch (error) {
|
||||
console.error('加载快速回复列表失败:', error)
|
||||
} finally {
|
||||
|
||||
@@ -106,10 +106,8 @@ async function loadPerformance() {
|
||||
params.date_to = dateRange.value[1]
|
||||
}
|
||||
|
||||
const { data } = await getAgentPerformance(params)
|
||||
if (data.code === 0) {
|
||||
agents.value = data.data.items || []
|
||||
}
|
||||
const data = await getAgentPerformance(params)
|
||||
agents.value = data.items || []
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载绩效数据失败')
|
||||
} finally {
|
||||
|
||||
@@ -87,7 +87,7 @@ const currentMode = ref<string>('manual')
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await getAssignmentMode()
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
currentMode.value = data.current_mode
|
||||
if (data.modes && data.modes.length > 0) {
|
||||
modes.value = data.modes
|
||||
|
||||
@@ -188,11 +188,9 @@ function buildParams(): Record<string, any> {
|
||||
async function loadLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getAuditLogs(buildParams())
|
||||
if (data.code === 0) {
|
||||
logs.value = data.data.items || []
|
||||
pagination.total = data.data.total || 0
|
||||
}
|
||||
const data = await getAuditLogs(buildParams())
|
||||
logs.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载安全审计日志失败')
|
||||
} finally {
|
||||
|
||||
@@ -216,20 +216,18 @@ const dissatisfiedPercentage = computed(() => {
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getEvaluationStats({
|
||||
const data = await getEvaluationStats({
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
if (data.code === 0) {
|
||||
stats.value = {
|
||||
total_count: data.data.total_count,
|
||||
avg_star_rating: data.data.avg_star_rating,
|
||||
star_distribution: data.data.star_distribution || [],
|
||||
emoji_distribution: data.data.emoji_distribution || [],
|
||||
}
|
||||
records.value = data.data.recent_evaluations || []
|
||||
total.value = data.data.total_count
|
||||
stats.value = {
|
||||
total_count: data.total_count,
|
||||
avg_star_rating: data.avg_star_rating,
|
||||
star_distribution: data.star_distribution || [],
|
||||
emoji_distribution: data.emoji_distribution || [],
|
||||
}
|
||||
records.value = data.recent_evaluations || []
|
||||
total.value = data.total_count
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载评价统计数据失败')
|
||||
} finally {
|
||||
|
||||
@@ -262,7 +262,7 @@ onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getIntegrations()
|
||||
integrations.value = response.data.data.items
|
||||
integrations.value = response.items
|
||||
} catch {
|
||||
// 使用默认 demo 数据
|
||||
integrations.value = getDefaultIntegrations()
|
||||
@@ -522,7 +522,7 @@ async function handleTestLianruanConnection(): Promise<void> {
|
||||
|
||||
// 再测试连接
|
||||
const response = await testLianruanConnection()
|
||||
testResult.value = response.data.data
|
||||
testResult.value = response
|
||||
testResultVisible.value = true
|
||||
|
||||
// 如果测试成功,更新本地状态
|
||||
@@ -541,8 +541,9 @@ async function handleTestLianruanConnection(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
|
||||
} catch (error: any) {
|
||||
const msg = error?.message || error?.response?.data?.message || '请求失败,请检查网络连接'
|
||||
testResult.value = { success: false, message: msg }
|
||||
testResultVisible.value = true
|
||||
} finally {
|
||||
testingConnection.value = false
|
||||
@@ -568,7 +569,7 @@ async function handleTestHuorongConnection(): Promise<void> {
|
||||
|
||||
// 再测试连接
|
||||
const response = await testHuorongConnection()
|
||||
testResult.value = response.data.data
|
||||
testResult.value = response
|
||||
testResultVisible.value = true
|
||||
|
||||
// 如果测试成功,更新本地状态
|
||||
@@ -588,8 +589,9 @@ async function handleTestHuorongConnection(): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
|
||||
} catch (error: any) {
|
||||
const msg = error?.message || error?.response?.data?.message || '请求失败,请检查网络连接'
|
||||
testResult.value = { success: false, message: msg }
|
||||
testResultVisible.value = true
|
||||
} finally {
|
||||
testingConnection.value = false
|
||||
@@ -608,7 +610,7 @@ async function handleTest(integration: Integration): Promise<void> {
|
||||
testingConnection.value = true
|
||||
try {
|
||||
const response = await testRagflowConnection()
|
||||
testResult.value = response.data.data
|
||||
testResult.value = response
|
||||
testResultVisible.value = true
|
||||
|
||||
// 如果测试成功,更新本地状态
|
||||
@@ -621,8 +623,9 @@ async function handleTest(integration: Integration): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
|
||||
} catch (error: any) {
|
||||
const msg = error?.message || error?.response?.data?.message || '请求失败,请检查网络连接'
|
||||
testResult.value = { success: false, message: msg }
|
||||
testResultVisible.value = true
|
||||
} finally {
|
||||
testingConnection.value = false
|
||||
|
||||
@@ -200,11 +200,13 @@ async function handleLoginSuccess(token: string, userInfo?: any): Promise<void>
|
||||
if (userInfo) {
|
||||
localStorage.setItem('admin_user_id', userInfo.userid)
|
||||
adminStore.adminUserId = userInfo.userid
|
||||
adminStore.adminInfo = {
|
||||
const adminInfoData = {
|
||||
user_id: userInfo.userid,
|
||||
name: userInfo.name,
|
||||
status: 'online',
|
||||
}
|
||||
adminStore.adminInfo = adminInfoData
|
||||
localStorage.setItem('admin_info', JSON.stringify(adminInfoData))
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
@@ -244,11 +246,13 @@ async function processOAuthCallback(code: string, state?: string): Promise<void>
|
||||
adminStore.token = result.token
|
||||
localStorage.setItem('admin_user_id', result.user_info.userid)
|
||||
adminStore.adminUserId = result.user_info.userid
|
||||
adminStore.adminInfo = {
|
||||
const adminInfoData = {
|
||||
user_id: result.user_info.userid,
|
||||
name: result.user_info.name,
|
||||
status: 'online',
|
||||
}
|
||||
adminStore.adminInfo = adminInfoData
|
||||
localStorage.setItem('admin_info', JSON.stringify(adminInfoData))
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
|
||||
@@ -128,7 +128,7 @@ onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getMonitorSessions()
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
Object.assign(stats, data.stats)
|
||||
sessions.value = data.items
|
||||
} catch {
|
||||
|
||||
@@ -171,11 +171,9 @@ function buildParams(download = false): Record<string, any> {
|
||||
async function loadLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getRuntimeLogs(buildParams())
|
||||
if (data.code === 0) {
|
||||
logs.value = data.data.items || []
|
||||
pagination.total = data.data.total || 0
|
||||
}
|
||||
const data = await getRuntimeLogs(buildParams())
|
||||
logs.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载运行期日志失败')
|
||||
} finally {
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
<el-option label="服务中" value="serving" />
|
||||
<el-option label="已结单" value="resolved" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.is_archived" placeholder="归档状态" clearable style="width: 140px">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="未归档" value="false" />
|
||||
<el-option label="已归档" value="true" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
@@ -52,6 +57,12 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_archived" label="归档" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_archived" type="info" size="small">已归档</el-tag>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assigned_agent_name" label="负责坐席" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.assigned_agent_name || '—' }}
|
||||
@@ -166,6 +177,7 @@ const dateRange = ref<[string, string] | null>(null)
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
is_archived: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -184,16 +196,15 @@ async function loadConversations() {
|
||||
}
|
||||
if (filters.keyword) params.keyword = filters.keyword
|
||||
if (filters.status) params.status = filters.status
|
||||
if (filters.is_archived !== '') params.is_archived = filters.is_archived === 'true' // 字符串转布尔
|
||||
if (dateRange.value) {
|
||||
params.date_from = dateRange.value[0]
|
||||
params.date_to = dateRange.value[1]
|
||||
}
|
||||
|
||||
const { data } = await getAuditConversations(params)
|
||||
if (data.code === 0) {
|
||||
conversations.value = data.data.items || []
|
||||
pagination.total = data.data.total || 0
|
||||
}
|
||||
const data = await getAuditConversations(params)
|
||||
conversations.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载会话列表失败')
|
||||
} finally {
|
||||
@@ -206,10 +217,8 @@ async function showDetail(row: any) {
|
||||
drawerVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const { data } = await getAuditConversationDetail(row.id)
|
||||
if (data.code === 0) {
|
||||
conversationDetail.value = data.data
|
||||
}
|
||||
const data = await getAuditConversationDetail(row.id)
|
||||
conversationDetail.value = data
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载会话详情失败')
|
||||
} finally {
|
||||
@@ -226,6 +235,7 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.is_archived = ''
|
||||
dateRange.value = null
|
||||
pagination.page = 1
|
||||
loadConversations()
|
||||
|
||||
@@ -149,11 +149,9 @@ function buildParams(): Record<string, any> {
|
||||
async function loadLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getSystemLogs(buildParams())
|
||||
if (data.code === 0) {
|
||||
logs.value = data.data.items || []
|
||||
pagination.total = data.data.total || 0
|
||||
}
|
||||
const data = await getSystemLogs(buildParams())
|
||||
logs.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} catch (err: any) {
|
||||
ElMessage.error('加载配置变更历史失败')
|
||||
} finally {
|
||||
|
||||
@@ -503,7 +503,7 @@ async function loadStats(): Promise<void> {
|
||||
try {
|
||||
// 1. 从 _leak 接口获取高危终端数和终端总数
|
||||
const leakResp = await getHuorongLeaks({ page: 1, per_page: 1 })
|
||||
const leakData = leakResp.data.data
|
||||
const leakData = leakResp
|
||||
if (!leakData.error) {
|
||||
stats.high_risk_terminals = leakData.risk_client || 0
|
||||
if (leakData.all_client) {
|
||||
@@ -513,7 +513,7 @@ async function loadStats(): Promise<void> {
|
||||
|
||||
// 2. 从 _virus_events 接口获取病毒事件统计(type=2=全部)
|
||||
const virusResp = await getHuorongVirusEvents({ page: 1, per_page: 200 })
|
||||
const virusData = virusResp.data.data
|
||||
const virusData = virusResp
|
||||
if (!virusData.error) {
|
||||
const items: HuorongVirusEvent[] = virusData.items || []
|
||||
// 统计有处理失败病毒事件的终端数
|
||||
@@ -553,7 +553,7 @@ async function loadTerminals(): Promise<void> {
|
||||
page: currentPage.value,
|
||||
per_page: pageSize.value,
|
||||
})
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
|
||||
// 检查是否有错误(API未配置等)
|
||||
if (data.error) {
|
||||
@@ -588,7 +588,7 @@ async function loadLeaks(): Promise<void> {
|
||||
page: currentPage.value,
|
||||
per_page: pageSize.value,
|
||||
})
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
|
||||
if (data.error) {
|
||||
connectionError.value = data.error
|
||||
@@ -616,7 +616,7 @@ async function loadVirusEvents(): Promise<void> {
|
||||
page: currentPage.value,
|
||||
per_page: pageSize.value,
|
||||
})
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
|
||||
if (data.error) {
|
||||
connectionError.value = data.error
|
||||
@@ -692,7 +692,7 @@ async function showTerminalDetail(row: HuorongTerminal): Promise<void> {
|
||||
|
||||
try {
|
||||
const response = await getHuorongTerminalDetail(row.client_id)
|
||||
const data = response.data.data
|
||||
const data = response
|
||||
|
||||
if (data.error) {
|
||||
// API错误,用基本数据展示
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
企微IT智能服务台 — 欢迎与引导配置页面
|
||||
=============================================================================
|
||||
说明:管理员工端H5的欢迎页、引导视频、互动教学、主题模板配置
|
||||
-->
|
||||
<template>
|
||||
<div class="welcome-config-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-title">欢迎与引导配置</div>
|
||||
<div class="page-desc">统一管理员工端H5的欢迎内容、引导视频、互动教学和主题模板</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<el-skeleton :rows="8" animated />
|
||||
</div>
|
||||
|
||||
<!-- 配置内容 -->
|
||||
<div v-else class="config-content">
|
||||
<!-- Tab 导航 -->
|
||||
<el-tabs v-model="activeTab" class="config-tabs">
|
||||
<!-- Tab 1: 欢迎页 -->
|
||||
<el-tab-pane label="欢迎页" name="welcome">
|
||||
<div class="tab-content">
|
||||
<el-form label-width="120px" label-position="top">
|
||||
<!-- 启用开关 -->
|
||||
<el-form-item label="启用欢迎页">
|
||||
<el-switch
|
||||
v-model="config.welcome_page.enabled"
|
||||
@change="handleSave"
|
||||
/>
|
||||
<span class="form-tip">关闭后员工首次访问将不显示欢迎页</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 标题 -->
|
||||
<el-form-item label="欢迎标题">
|
||||
<el-input
|
||||
v-model="config.welcome_page.title"
|
||||
placeholder="请输入欢迎标题"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
:disabled="!config.welcome_page.enabled"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 欢迎文案 -->
|
||||
<el-form-item label="欢迎文案">
|
||||
<el-input
|
||||
v-model="config.welcome_page.content"
|
||||
type="textarea"
|
||||
placeholder="请输入欢迎文案"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="!config.welcome_page.enabled"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<el-form-item label="背景颜色">
|
||||
<div class="color-picker-wrapper">
|
||||
<el-color-picker
|
||||
v-model="config.welcome_page.background_color"
|
||||
:disabled="!config.welcome_page.enabled"
|
||||
/>
|
||||
<el-input
|
||||
v-model="config.welcome_page.background_color"
|
||||
class="color-input"
|
||||
:disabled="!config.welcome_page.enabled"
|
||||
/>
|
||||
</div>
|
||||
<span class="form-tip">选择欢迎页的背景颜色</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 预览效果 -->
|
||||
<el-form-item label="预览效果">
|
||||
<div
|
||||
class="welcome-preview"
|
||||
:style="{ backgroundColor: config.welcome_page.background_color }"
|
||||
>
|
||||
<h2>{{ config.welcome_page.title || '欢迎标题' }}</h2>
|
||||
<p>{{ config.welcome_page.content || '欢迎文案内容...' }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSave">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 引导视频 -->
|
||||
<el-tab-pane label="引导视频" name="video">
|
||||
<div class="tab-content">
|
||||
<!-- 当前视频信息 -->
|
||||
<div v-if="config.welcome_video" class="video-info">
|
||||
<el-card class="video-card">
|
||||
<template #header>
|
||||
<div class="video-card-header">
|
||||
<span>当前视频</span>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDeleteVideo"
|
||||
>
|
||||
删除视频
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="video-player">
|
||||
<video
|
||||
:src="config.welcome_video.url"
|
||||
controls
|
||||
class="video-element"
|
||||
/>
|
||||
</div>
|
||||
<div class="video-meta">
|
||||
<p><strong>文件名:</strong>{{ config.welcome_video.filename }}</p>
|
||||
<p><strong>上传时间:</strong>{{ formatDate(config.welcome_video.uploaded_at) }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 无视频提示 -->
|
||||
<el-empty v-else description="暂无引导视频" />
|
||||
|
||||
<!-- 上传视频 -->
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="video-uploader"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".mp4"
|
||||
:on-change="handleVideoChange"
|
||||
:on-exceed="handleExceed"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<el-icon><Upload /></el-icon>
|
||||
选择视频
|
||||
</el-button>
|
||||
<template #tip>
|
||||
<div class="upload-tip">
|
||||
支持 .mp4 格式,最大 50MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<el-button
|
||||
v-if="uploadFile"
|
||||
type="success"
|
||||
:loading="uploading"
|
||||
@click="handleUploadVideo"
|
||||
>
|
||||
{{ uploading ? '上传中...' : '上传视频' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 3: 互动教学 -->
|
||||
<el-tab-pane label="互动教学" name="tutorial">
|
||||
<div class="tab-content">
|
||||
<el-form label-width="120px" label-position="top">
|
||||
<!-- 启用开关 -->
|
||||
<el-form-item label="启用互动教学">
|
||||
<el-switch
|
||||
v-model="config.tutorial.enabled"
|
||||
@change="handleSave"
|
||||
/>
|
||||
<span class="form-tip">开启后员工端将显示交互式使用教程</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 教程步骤列表 -->
|
||||
<el-form-item label="教程步骤">
|
||||
<div class="steps-container">
|
||||
<div
|
||||
v-for="(element, index) in config.tutorial.steps"
|
||||
:key="element.id"
|
||||
class="step-item"
|
||||
>
|
||||
<div class="step-number">{{ index + 1 }}</div>
|
||||
<div class="step-content">
|
||||
<el-input
|
||||
v-model="element.title"
|
||||
placeholder="步骤标题"
|
||||
:disabled="!config.tutorial.enabled"
|
||||
class="step-title-input"
|
||||
/>
|
||||
<el-input
|
||||
v-model="element.description"
|
||||
type="textarea"
|
||||
placeholder="步骤说明"
|
||||
:rows="2"
|
||||
:disabled="!config.tutorial.enabled"
|
||||
class="step-desc-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<el-button
|
||||
:icon="ArrowUp"
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!config.tutorial.enabled || index === 0"
|
||||
@click="moveStepUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:icon="ArrowDown"
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!config.tutorial.enabled || index === config.tutorial.steps.length - 1"
|
||||
@click="moveStepDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
:disabled="!config.tutorial.enabled"
|
||||
@click="removeStep(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:disabled="!config.tutorial.enabled"
|
||||
@click="addStep"
|
||||
>
|
||||
添加步骤
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSave">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 4: 主题模板 -->
|
||||
<el-tab-pane label="主题模板" name="theme">
|
||||
<div class="tab-content">
|
||||
<el-form label-width="120px" label-position="top">
|
||||
<el-form-item label="选择主题">
|
||||
<el-radio-group
|
||||
v-model="config.theme.theme"
|
||||
class="theme-radio-group"
|
||||
@change="handleSave"
|
||||
>
|
||||
<el-radio value="default">
|
||||
<div class="theme-option">
|
||||
<div class="theme-color" style="background-color: #07C160" />
|
||||
<span>默认绿色</span>
|
||||
</div>
|
||||
</el-radio>
|
||||
<el-radio value="blue">
|
||||
<div class="theme-option">
|
||||
<div class="theme-color" style="background-color: #1989FA" />
|
||||
<span>商务蓝色</span>
|
||||
</div>
|
||||
</el-radio>
|
||||
<el-radio value="orange">
|
||||
<div class="theme-option">
|
||||
<div class="theme-color" style="background-color: #E6A23C" />
|
||||
<span>活力橙色</span>
|
||||
</div>
|
||||
</el-radio>
|
||||
<el-radio value="red">
|
||||
<div class="theme-option">
|
||||
<div class="theme-color" style="background-color: #F56C6C" />
|
||||
<span>热情红色</span>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 预览效果 -->
|
||||
<el-form-item label="预览效果">
|
||||
<div
|
||||
class="theme-preview"
|
||||
:style="{ backgroundColor: THEME_COLORS[config.theme.theme] || THEME_COLORS.default }"
|
||||
>
|
||||
<div class="theme-preview-content">
|
||||
<h3>员工端H5主题预览</h3>
|
||||
<p>当前主题:{{ THEME_LABELS[config.theme.theme] }}</p>
|
||||
<div class="preview-button">主要按钮</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSave">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// ==========================================================================
|
||||
// 依赖导入
|
||||
// ==========================================================================
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Upload, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getWelcomeConfig,
|
||||
updateWelcomeConfig,
|
||||
uploadWelcomeVideo,
|
||||
deleteWelcomeVideo,
|
||||
THEME_COLORS,
|
||||
THEME_LABELS,
|
||||
} from '@/api/welcome'
|
||||
import type { WelcomeConfig, TutorialStep } from '@/api/welcome'
|
||||
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
const loading = ref<boolean>(true)
|
||||
const uploading = ref<boolean>(false)
|
||||
const uploadFile = ref<File | null>(null)
|
||||
const activeTab = ref<string>('welcome')
|
||||
|
||||
// 配置文件
|
||||
const config = reactive<WelcomeConfig>({
|
||||
welcome_page: {
|
||||
enabled: false,
|
||||
title: '',
|
||||
content: '',
|
||||
background_color: '#07C160',
|
||||
},
|
||||
welcome_video: null,
|
||||
tutorial: {
|
||||
enabled: false,
|
||||
steps: [],
|
||||
},
|
||||
theme: {
|
||||
theme: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 生命周期
|
||||
// ==========================================================================
|
||||
onMounted(async () => {
|
||||
await loadConfig()
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 数据加载
|
||||
// ==========================================================================
|
||||
async function loadConfig(): Promise<void> {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getWelcomeConfig()
|
||||
const data = response.data.data
|
||||
|
||||
// 合并配置
|
||||
if (data.welcome_page) {
|
||||
Object.assign(config.welcome_page, data.welcome_page)
|
||||
}
|
||||
if (data.welcome_video) {
|
||||
config.welcome_video = data.welcome_video
|
||||
}
|
||||
if (data.tutorial) {
|
||||
Object.assign(config.tutorial, data.tutorial)
|
||||
}
|
||||
if (data.theme) {
|
||||
Object.assign(config.theme, data.theme)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
ElMessage.error('加载配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 配置保存
|
||||
// ==========================================================================
|
||||
async function handleSave(): Promise<void> {
|
||||
try {
|
||||
await updateWelcomeConfig(config)
|
||||
ElMessage.success('配置已保存')
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
ElMessage.error('保存配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 视频上传
|
||||
// ==========================================================================
|
||||
function handleVideoChange(file: { raw: File }): void {
|
||||
uploadFile.value = file.raw
|
||||
}
|
||||
|
||||
function handleExceed(): void {
|
||||
ElMessage.warning('只能上传一个视频,请先删除当前视频')
|
||||
}
|
||||
|
||||
async function handleUploadVideo(): Promise<void> {
|
||||
if (!uploadFile.value) return
|
||||
|
||||
try {
|
||||
uploading.value = true
|
||||
const response = await uploadWelcomeVideo(uploadFile.value)
|
||||
const data = response.data.data
|
||||
|
||||
config.welcome_video = {
|
||||
url: data.url,
|
||||
filename: data.filename,
|
||||
uploaded_at: data.uploaded_at,
|
||||
}
|
||||
|
||||
uploadFile.value = null
|
||||
ElMessage.success('视频上传成功')
|
||||
} catch (error) {
|
||||
console.error('上传视频失败:', error)
|
||||
ElMessage.error('上传视频失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteVideo(): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除当前视频吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
|
||||
await deleteWelcomeVideo()
|
||||
config.welcome_video = null
|
||||
uploadFile.value = null
|
||||
ElMessage.success('视频已删除')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除视频失败:', error)
|
||||
ElMessage.error('删除视频失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 互动教学步骤管理
|
||||
// ==========================================================================
|
||||
function addStep(): void {
|
||||
const newStep: TutorialStep = {
|
||||
id: `step_${Date.now()}`,
|
||||
title: '',
|
||||
description: '',
|
||||
}
|
||||
config.tutorial.steps.push(newStep)
|
||||
}
|
||||
|
||||
function removeStep(index: number): void {
|
||||
config.tutorial.steps.splice(index, 1)
|
||||
}
|
||||
|
||||
function moveStepUp(index: number): void {
|
||||
if (index > 0) {
|
||||
const temp = config.tutorial.steps[index]
|
||||
config.tutorial.steps[index] = config.tutorial.steps[index - 1]
|
||||
config.tutorial.steps[index - 1] = temp
|
||||
}
|
||||
}
|
||||
|
||||
function moveStepDown(index: number): void {
|
||||
if (index < config.tutorial.steps.length - 1) {
|
||||
const temp = config.tutorial.steps[index]
|
||||
config.tutorial.steps[index] = config.tutorial.steps[index + 1]
|
||||
config.tutorial.steps[index + 1] = temp
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 工具函数
|
||||
// ==========================================================================
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.welcome-config-page {
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.config-tabs {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 欢迎页预览 */
|
||||
.welcome-preview {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.welcome-preview h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.welcome-preview p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 颜色选择器 */
|
||||
.color-picker-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
/* 视频上传 */
|
||||
.video-info {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.video-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
border-radius: 8px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-meta p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 互动教学步骤 */
|
||||
.steps-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.step-title-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.step-desc-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 主题选择 */
|
||||
.theme-radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-color {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-preview-content h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.theme-preview-content p {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-button {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user