chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
+251
View File
@@ -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 || '')
/** 坐席IDuser_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
+177
View File
@@ -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,
}
})
+54
View File
@@ -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,
}
})
+123
View File
@@ -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,
}
})