chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 坐席状态管理(Pinia Store)
|
||||
// =============================================================================
|
||||
// 说明:管理坐席登录状态、当前坐席信息、坐席状态切换
|
||||
// 核心功能:
|
||||
// 1. 当前登录坐席信息
|
||||
// 2. 登录/登出方法
|
||||
// 3. 坐席状态(online/busy/offline)
|
||||
// 4. Token 管理(localStorage 存储)
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Agent } from '@/api/agent'
|
||||
import { login as apiLogin, getCurrentAgent, updateAgentStatus, getAgents } from '@/api/agent'
|
||||
import { mockLoginData, mockCurrentAgent, mockAgentListData } from '@/mock/data'
|
||||
import router from '@/router'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Token 存储 key
|
||||
// --------------------------------------------------------------------------
|
||||
const TOKEN_KEY = 'agent_token'
|
||||
const PORTAL_TOKEN_KEY = 'portal_token'
|
||||
const AGENT_USER_ID_KEY = 'agent_user_id'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
// --------------------------------------------------------------------------
|
||||
export const useAgentStore = defineStore('agent', () => {
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 当前登录的坐席信息 */
|
||||
const agentInfo = ref<Agent | null>(null)
|
||||
|
||||
/** 认证 token — 优先从 agent_token 读取,降级读取 portal_token */
|
||||
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY) || localStorage.getItem(PORTAL_TOKEN_KEY))
|
||||
|
||||
/** 坐席用户ID */
|
||||
const agentUserId = ref<string | null>(localStorage.getItem(AGENT_USER_ID_KEY))
|
||||
|
||||
/** 是否正在登录 */
|
||||
const logging = ref<boolean>(false)
|
||||
|
||||
/** 可转接的坐席列表(用于转接功能) */
|
||||
const availableAgents = ref<Agent[]>([])
|
||||
|
||||
// ==========================================================================
|
||||
// 计算属性
|
||||
// ==========================================================================
|
||||
|
||||
/** 是否已登录 */
|
||||
const isLoggedIn = computed(() => !!token.value && !!agentInfo.value)
|
||||
|
||||
/** 坐席状态 */
|
||||
const agentStatus = computed(() => agentInfo.value?.status || 'offline')
|
||||
|
||||
/** 坐席姓名 */
|
||||
const agentName = computed(() => agentInfo.value?.name || '')
|
||||
|
||||
/** 坐席ID(user_id) */
|
||||
const userId = computed(() => agentInfo.value?.user_id || agentUserId.value || '')
|
||||
|
||||
// ==========================================================================
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 坐席登录
|
||||
* 调用后端登录 API,获取坐席信息和 token
|
||||
* admin 角色需要 OTP 二次验证
|
||||
* 登录成功后自动跳转到工作台页面
|
||||
*
|
||||
* @param inputUserId - 企微用户ID
|
||||
* @param inputName - 坐席姓名
|
||||
* @param otpCode - OTP 动态码(可选)
|
||||
* @returns 登录数据(包含 require_otp 标记)
|
||||
*/
|
||||
async function login(inputUserId: string, inputName: string, otpCode?: string): Promise<any> {
|
||||
try {
|
||||
logging.value = true
|
||||
const data = await apiLogin(inputUserId, inputName, otpCode)
|
||||
|
||||
// 检查是否需要 OTP 验证
|
||||
if ('require_otp' in data && data.require_otp) {
|
||||
// 返回 data,让 Login.vue 处理 require_otp
|
||||
logging.value = false
|
||||
return data
|
||||
}
|
||||
|
||||
// 保存登录信息
|
||||
token.value = data.token
|
||||
agentUserId.value = data.user_id
|
||||
localStorage.setItem(TOKEN_KEY, data.token)
|
||||
localStorage.setItem(AGENT_USER_ID_KEY, data.user_id)
|
||||
|
||||
// 更新 Axios 默认请求头(添加 Authorization)
|
||||
// 注意:apiClient 拦截器中会从 localStorage 读取 token
|
||||
|
||||
// 保存坐席信息(去掉 token 字段)
|
||||
const { token: _token, ...agentData } = data
|
||||
agentInfo.value = agentData as Agent
|
||||
|
||||
// 跳转到工作台
|
||||
router.push('/workspace')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
// 使用 mock 数据作为 fallback(开发/演示用)
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[Mock] 使用模拟登录数据')
|
||||
const { token: _t, ...agentData } = mockLoginData
|
||||
token.value = mockLoginData.token
|
||||
agentUserId.value = mockLoginData.user_id
|
||||
agentInfo.value = agentData as Agent
|
||||
localStorage.setItem(TOKEN_KEY, mockLoginData.token)
|
||||
localStorage.setItem(AGENT_USER_ID_KEY, mockLoginData.user_id)
|
||||
router.push('/workspace')
|
||||
return
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
logging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐席登出
|
||||
* 清除本地存储的登录信息,跳转到登录页
|
||||
*/
|
||||
function logout(): void {
|
||||
// 清除状态
|
||||
token.value = null
|
||||
agentUserId.value = null
|
||||
agentInfo.value = null
|
||||
|
||||
// 清除 localStorage
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(AGENT_USER_ID_KEY)
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前坐席信息
|
||||
* 从后端获取最新的坐席数据
|
||||
*/
|
||||
async function refreshAgentInfo(): Promise<void> {
|
||||
try {
|
||||
if (!token.value) return
|
||||
const data = await getCurrentAgent()
|
||||
agentInfo.value = data
|
||||
} catch (error) {
|
||||
console.error('获取坐席信息失败:', error)
|
||||
// 使用 mock 数据作为 fallback(开发/演示用)
|
||||
if (import.meta.env.DEV && !agentInfo.value) {
|
||||
console.warn('[Mock] 使用模拟坐席信息')
|
||||
agentInfo.value = mockCurrentAgent
|
||||
}
|
||||
// 如果是 401 未授权,说明 token 过期,需要重新登录
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
const axiosError = error as { response?: { status?: number } }
|
||||
if (axiosError.response?.status === 401) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换坐席状态
|
||||
*
|
||||
* @param newStatus - 新状态: online/busy/offline
|
||||
*/
|
||||
async function changeStatus(newStatus: string): Promise<void> {
|
||||
try {
|
||||
const data = await updateAgentStatus(newStatus)
|
||||
agentInfo.value = data
|
||||
} catch (error) {
|
||||
console.error('更新坐席状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载可转接的坐席列表
|
||||
* 只获取在线的坐席(排除自己)
|
||||
*/
|
||||
async function loadAvailableAgents(): Promise<void> {
|
||||
try {
|
||||
const data = await getAgents('online')
|
||||
// 排除自己
|
||||
availableAgents.value = data.items.filter(
|
||||
a => a.user_id !== agentInfo.value?.user_id
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('获取坐席列表失败:', error)
|
||||
// 使用 mock 数据作为 fallback(开发/演示用)
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[Mock] 使用模拟坐席列表')
|
||||
availableAgents.value = mockAgentListData.items.filter(
|
||||
a => a.user_id !== agentInfo.value?.user_id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:检查是否已登录
|
||||
* 如果 localStorage 有 token,尝试获取坐席信息
|
||||
*/
|
||||
async function initAuth(): Promise<void> {
|
||||
const savedToken = localStorage.getItem(TOKEN_KEY)
|
||||
if (savedToken) {
|
||||
token.value = savedToken
|
||||
agentUserId.value = localStorage.getItem(AGENT_USER_ID_KEY)
|
||||
try {
|
||||
await refreshAgentInfo()
|
||||
} catch {
|
||||
// token 无效,清除
|
||||
logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 返回
|
||||
// ==========================================================================
|
||||
return {
|
||||
// 状态
|
||||
agentInfo,
|
||||
token,
|
||||
agentUserId,
|
||||
logging,
|
||||
availableAgents,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
agentStatus,
|
||||
agentName,
|
||||
userId,
|
||||
|
||||
// 方法
|
||||
login,
|
||||
logout,
|
||||
refreshAgentInfo,
|
||||
changeStatus,
|
||||
loadAvailableAgents,
|
||||
initAuth,
|
||||
}
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 快速回复状态管理(Pinia Store)
|
||||
// =============================================================================
|
||||
// 说明:管理快速回复模板列表、按分类展示、CRUD 操作
|
||||
// 核心功能:
|
||||
// 1. 模板列表(按分类)
|
||||
// 2. CRUD 操作(创建、读取、更新、删除)
|
||||
// 3. 变量替换({employee_name} 等)
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { QuickReply } from '@/api/quickReply'
|
||||
import {
|
||||
getQuickReplies,
|
||||
createQuickReply,
|
||||
updateQuickReply,
|
||||
deleteQuickReply,
|
||||
} from '@/api/quickReply'
|
||||
import type { QuickReplyCreateParams, QuickReplyUpdateParams } from '@/api/quickReply'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
// --------------------------------------------------------------------------
|
||||
export const useQuickReplyStore = defineStore('quickReply', () => {
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 所有快速回复模板列表 */
|
||||
const templates = ref<QuickReply[]>([])
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// ==========================================================================
|
||||
// 计算属性
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 按分类分组的模板
|
||||
* 返回 { 分类名: 模板列表 } 的结构,用于 ElCollapse 折叠展示
|
||||
*/
|
||||
const templatesByCategory = computed(() => {
|
||||
const grouped: Record<string, QuickReply[]> = {}
|
||||
for (const tpl of templates.value) {
|
||||
if (!grouped[tpl.category]) {
|
||||
grouped[tpl.category] = []
|
||||
}
|
||||
grouped[tpl.category].push(tpl)
|
||||
}
|
||||
return grouped
|
||||
})
|
||||
|
||||
/**
|
||||
* 所有分类名列表(去重)
|
||||
*/
|
||||
const categories = computed(() => {
|
||||
return Object.keys(templatesByCategory.value)
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 加载快速回复模板列表
|
||||
* 从后端 API 获取所有模板数据
|
||||
*/
|
||||
async function fetchTemplates(): Promise<void> {
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await getQuickReplies()
|
||||
templates.value = data.items
|
||||
} catch (error) {
|
||||
console.error('获取快速回复模板失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建快速回复模板
|
||||
*
|
||||
* @param data - 创建参数
|
||||
* @returns 创建的模板
|
||||
*/
|
||||
async function addTemplate(data: QuickReplyCreateParams): Promise<QuickReply | null> {
|
||||
try {
|
||||
const newTemplate = await createQuickReply(data)
|
||||
// 重新加载列表
|
||||
await fetchTemplates()
|
||||
return newTemplate
|
||||
} catch (error) {
|
||||
console.error('创建快速回复模板失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新快速回复模板
|
||||
*
|
||||
* @param templateId - 模板ID
|
||||
* @param data - 更新参数
|
||||
* @returns 更新后的模板
|
||||
*/
|
||||
async function editTemplate(templateId: string, data: QuickReplyUpdateParams): Promise<QuickReply | null> {
|
||||
try {
|
||||
const updated = await updateQuickReply(templateId, data)
|
||||
// 重新加载列表
|
||||
await fetchTemplates()
|
||||
return updated
|
||||
} catch (error) {
|
||||
console.error('更新快速回复模板失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除快速回复模板
|
||||
*
|
||||
* @param templateId - 模板ID
|
||||
*/
|
||||
async function removeTemplate(templateId: string): Promise<void> {
|
||||
try {
|
||||
await deleteQuickReply(templateId)
|
||||
// 重新加载列表
|
||||
await fetchTemplates()
|
||||
} catch (error) {
|
||||
console.error('删除快速回复模板失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换模板中的变量
|
||||
* 支持 {employee_name}、{department} 等变量占位符
|
||||
*
|
||||
* @param templateContent - 模板内容(含变量占位符)
|
||||
* @param variables - 变量键值对,如 { employee_name: '张三', department: '技术部' }
|
||||
* @returns 替换后的内容
|
||||
*
|
||||
* @example
|
||||
* replaceVariables('您好 {employee_name},您的问题是...', { employee_name: '张三' })
|
||||
* // 返回: '您好 张三,您的问题是...'
|
||||
*/
|
||||
function replaceVariables(
|
||||
templateContent: string,
|
||||
variables: Record<string, string>
|
||||
): string {
|
||||
let result = templateContent
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
// 使用全局替换,替换所有 {key} 形式的占位符
|
||||
result = result.replaceAll(`{${key}}`, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 返回
|
||||
// ==========================================================================
|
||||
return {
|
||||
// 状态
|
||||
templates,
|
||||
loading,
|
||||
|
||||
// 计算属性
|
||||
templatesByCategory,
|
||||
categories,
|
||||
|
||||
// 方法
|
||||
fetchTemplates,
|
||||
addTemplate,
|
||||
editTemplate,
|
||||
removeTemplate,
|
||||
replaceVariables,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 主题 Pinia Store
|
||||
// =============================================================================
|
||||
// 说明:管理全局主题状态(浅色/深色),提供 toggleTheme 和 initTheme 方法
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { applyTheme, getInitialTheme, type ThemeMode } from '@/composables/useTheme'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
// --------------------------------------------------------------------------
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 当前主题模式 */
|
||||
const currentTheme = ref<ThemeMode>('light')
|
||||
|
||||
// ==========================================================================
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
* 在浅色和深色之间切换,并持久化到 localStorage
|
||||
*/
|
||||
function toggleTheme(): void {
|
||||
const next: ThemeMode = currentTheme.value === 'light' ? 'dark' : 'light'
|
||||
currentTheme.value = next
|
||||
applyTheme(next)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
* 从 localStorage 读取已保存的主题偏好并应用
|
||||
*/
|
||||
function initTheme(): void {
|
||||
const theme = getInitialTheme()
|
||||
currentTheme.value = theme
|
||||
applyTheme(theme)
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 返回
|
||||
// ==========================================================================
|
||||
return {
|
||||
currentTheme,
|
||||
toggleTheme,
|
||||
initTheme,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 待办事项状态管理(Pinia Store)
|
||||
// =============================================================================
|
||||
// 说明:管理坐席工作台的待办事项列表、当前选中的待办、加载状态
|
||||
// 核心功能:
|
||||
// 1. 获取待办列表
|
||||
// 2. 选中待办事项(联动 conversationStore.workspaceView = 'task')
|
||||
// 3. 更新待办状态
|
||||
// =============================================================================
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { TodoItemData } from '@/api/todo'
|
||||
import { getTodoItems, updateTodoStatus } from '@/api/todo'
|
||||
import { mockTodoListData } from '@/mock/data'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store 定义
|
||||
// --------------------------------------------------------------------------
|
||||
export const useTodoStore = defineStore('todo', () => {
|
||||
// ==========================================================================
|
||||
// 响应式状态
|
||||
// ==========================================================================
|
||||
|
||||
/** 待办事项列表 */
|
||||
const todoList = ref<TodoItemData[]>([])
|
||||
|
||||
/** 当前选中的待办事项 */
|
||||
const currentTodoItem = ref<TodoItemData | null>(null)
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// ==========================================================================
|
||||
// 计算属性
|
||||
// ==========================================================================
|
||||
|
||||
/** 紧急待办数量 */
|
||||
const urgentCount = computed(() => {
|
||||
return todoList.value.filter(t => t.priority === 'urgent').length
|
||||
})
|
||||
|
||||
/** 高优先级待办数量 */
|
||||
const highCount = computed(() => {
|
||||
return todoList.value.filter(t => t.priority === 'high').length
|
||||
})
|
||||
|
||||
/** 待处理待办列表(状态为 pending 或 processing) */
|
||||
const pendingTodos = computed(() => {
|
||||
return todoList.value.filter(t => t.status === 'pending' || t.status === 'processing')
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 获取待办列表
|
||||
* 调用后端 API 获取当前坐席的待办事项
|
||||
*/
|
||||
async function fetchTodoList(): Promise<void> {
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await getTodoItems()
|
||||
todoList.value = data.items
|
||||
} catch (error) {
|
||||
console.error('获取待办列表失败:', error)
|
||||
// 使用 mock 数据作为 fallback(开发/演示用)
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('[Mock] 使用模拟待办数据')
|
||||
todoList.value = mockTodoListData.items
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中待办事项
|
||||
* 设置 currentTodoItem,并触发 workspaceView 切换为 'task'
|
||||
*
|
||||
* @param item - 要选中的待办事项
|
||||
*/
|
||||
function selectTodoItem(item: TodoItemData): void {
|
||||
currentTodoItem.value = item
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新待办状态
|
||||
*
|
||||
* @param id - 待办事项ID
|
||||
* @param status - 新状态
|
||||
*/
|
||||
async function updateTodoItemStatus(id: string, status: string): Promise<void> {
|
||||
try {
|
||||
await updateTodoStatus(id, status)
|
||||
// 刷新列表
|
||||
await fetchTodoList()
|
||||
} catch (error) {
|
||||
console.error('更新待办状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 返回
|
||||
// ==========================================================================
|
||||
return {
|
||||
// 状态
|
||||
todoList,
|
||||
currentTodoItem,
|
||||
loading,
|
||||
|
||||
// 计算属性
|
||||
urgentCount,
|
||||
highCount,
|
||||
pendingTodos,
|
||||
|
||||
// 方法
|
||||
fetchTodoList,
|
||||
selectTodoItem,
|
||||
updateTodoItemStatus,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user