feat: 审批流程模块 (T审批A审批)

- 新增 backend/app/api/approval.py 审批API
- 前端H5支持发起审批、审批操作
- 添加审批卡片弹窗组件
- 路由注册审批模块
This commit is contained in:
Simon
2026-06-15 09:32:41 +08:00
parent 64d6812ec3
commit 93ba41ed79
29 changed files with 6584 additions and 0 deletions
+32
View File
@@ -331,6 +331,38 @@ export async function getApprovalLinks(): Promise<ApprovalLink[]> {
return (data?.items || data || []) as ApprovalLink[]
}
// =============================================================================
// 审批流程关键词 API(新增 - 用于关键词触发卡片弹窗)
// =============================================================================
/** 审批关键词响应 */
export interface ApprovalKeyword {
keyword: string
template_id: string
template_name: string
type: 'jump' | 'api'
}
/**
* 获取审批关键词列表
* 用于前端关键词检测,触发卡片弹窗
* @returns 审批关键词数组
*/
export async function getApprovalKeywords(): Promise<ApprovalKeyword[]> {
const response: any = await apiClient.get('/approval/keywords')
return response.data || []
}
/**
* 生成跳转审批链接
* @param templateId 模板ID
* @returns 跳转链接
*/
export async function createApprovalJump(templateId: string): Promise<{ url: string; template_name: string }> {
const response: any = await apiClient.post('/approval/jump', { template_id: templateId })
return response.data
}
/**
* 获取软件下载列表
* 返回所有可下载的软件列表,按分类分组
@@ -0,0 +1,216 @@
<!-- =============================================================================
// 企微IT智能服务台 — 审批卡片弹窗组件
// =============================================================================
// 说明:关键词触发弹窗,展示审批选项供用户选择
// - 用户输入"申请"等关键词时弹出
// - 显示资源申请/设备申请等选项
// - 点击选项后跳转或提交
// ============================================================================= -->
<template>
<van-popup
v-model:show="visible"
position="bottom"
round
closeable
:style="{ height: '40%' }"
@close="handleClose"
>
<div class="approval-card">
<!-- 标题 -->
<div class="approval-card__header">
<div class="approval-card__title">选择审批类型</div>
<div class="approval-card__subtitle">根据您的需求选择相应的审批流程</div>
</div>
<!-- 选项列表 -->
<div class="approval-card__options">
<div
v-for="option in matchedOptions"
:key="option.template_id"
class="approval-card__option"
@click="handleSelect(option)"
>
<div class="approval-card__option-icon">
<van-icon :name="option.type === 'jump' ? 'link-o' : 'orders-o'" size="24" />
</div>
<div class="approval-card__option-content">
<div class="approval-card__option-title">{{ option.template_name }}</div>
<div class="approval-card__option-desc">
{{ option.type === 'jump' ? '跳转企微审批页面' : '填写表单提交审批' }}
</div>
</div>
<van-icon name="arrow" class="approval-card__option-arrow" />
</div>
</div>
</div>
</van-popup>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { showToast } from 'vant'
import { getApprovalKeywords, createApprovalJump, type ApprovalKeyword } from '@/api/conversation'
// Props
interface Props {
modelValue: boolean
triggerText?: string
}
const props = withDefaults(defineProps<Props>(), {
triggerText: '',
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'select': [option: ApprovalKeyword]
}>()
// 状态
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const approvalKeywords = ref<ApprovalKeyword[]>([])
const loading = ref(false)
// 匹配的审批选项
const matchedOptions = computed(() => {
if (!props.triggerText) return approvalKeywords.value
const text = props.triggerText.toLowerCase()
return approvalKeywords.value.filter((kw) => text.includes(kw.keyword.toLowerCase()))
})
// 加载审批关键词
async function loadKeywords() {
if (approvalKeywords.value.length > 0) return
try {
loading.value = true
approvalKeywords.value = await getApprovalKeywords()
} catch (error) {
console.error('加载审批关键词失败:', error)
} finally {
loading.value = false
}
}
// 选择审批选项
async function handleSelect(option: ApprovalKeyword) {
try {
if (option.type === 'jump') {
// 跳转审批
const result = await createApprovalJump(option.template_id)
// 在企微中打开链接
window.open(result.url, '_blank')
showToast('已打开审批页面')
} else {
// API提交 - 后续实现
showToast('该功能正在开发中')
}
emit('select', option)
handleClose()
} catch (error) {
console.error('打开审批失败:', error)
showToast('打开审批失败,请重试')
}
}
// 关闭弹窗
function handleClose() {
visible.value = false
}
// 监听显示
import { watch } from 'vue'
watch(
() => props.modelValue,
(val) => {
if (val) {
loadKeywords()
}
}
)
</script>
<style scoped>
.approval-card {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.approval-card__header {
text-align: center;
padding-bottom: 16px;
}
.approval-card__title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.approval-card__subtitle {
font-size: 13px;
color: var(--text-tertiary);
margin-top: 4px;
}
.approval-card__options {
flex: 1;
overflow-y: auto;
}
.approval-card__option {
display: flex;
align-items: center;
padding: 16px;
background: var(--bg-secondary);
border-radius: 12px;
margin-bottom: 12px;
cursor: pointer;
transition: background 0.2s;
}
.approval-card__option:active {
background: var(--bg-tertiary);
}
.approval-card__option-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--accent-color, #07c160);
display: flex;
align-items: center;
justify-content: center;
color: white;
margin-right: 12px;
}
.approval-card__option-content {
flex: 1;
}
.approval-card__option-title {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
}
.approval-card__option-desc {
font-size: 13px;
color: var(--text-tertiary);
margin-top: 2px;
}
.approval-card__option-arrow {
color: var(--text-tertiary);
}
</style>
@@ -111,6 +111,13 @@
@update:visible="showCallModal = $event"
@call-success="handleCallSuccess"
/>
<!-- 审批卡片弹窗关键词触发 -->
<ApprovalCardModal
v-model="store.approvalCardVisible"
:trigger-text="store.approvalCardTriggerText"
@select="handleApprovalSelect"
/>
</div>
</template>
@@ -128,6 +135,7 @@ import { useThemeStore } from '@/stores/theme'
import MessageBubble from './MessageBubble.vue'
import InputBar from './InputBar.vue'
import CallAgentModal from './CallAgentModal.vue'
import ApprovalCardModal from './ApprovalCardModal.vue'
import TroubleshootFlow from './TroubleshootFlow.vue'
import ParticipantList from './ParticipantList.vue'
@@ -171,6 +179,12 @@ function handleCallSuccess(): void {
store.fetchCurrentConversation()
}
/** 处理审批选项选择 */
function handleApprovalSelect(option: any): void {
console.log('[ChatPanel] 选择审批:', option)
store.closeApprovalCard()
}
// 监听消息列表变化,自动滚动到底部
watch(
() => store.messages.length,
@@ -30,6 +30,9 @@
<button class="input-box__tool-btn" title="截图" @click="handleScreenshot">
<span></span>
</button>
<button class="input-box__tool-btn input-box__tool-btn--accent" title="快捷申请" @click="handleQuickApply">
<span>📝</span>
</button>
</div>
<!-- 表情选择面板简易版常用 Emoji 网格 -->
@@ -466,6 +469,14 @@ function onScreenshotCancel(): void {
showScreenshotEditor.value = false
screenshotCanvas = null
}
// ============================================================================
// 快捷申请按钮
// ============================================================================
function handleQuickApply(): void {
// 触发审批卡片弹窗
store.showApprovalCard('')
}
</script>
<style scoped>
@@ -508,6 +519,17 @@ function onScreenshotCancel(): void {
border-color: var(--accent);
}
/* 快捷申请按钮 - 强调样式 */
.input-box__tool-btn--accent {
background: var(--accent);
border-color: var(--accent);
}
.input-box__tool-btn--accent:hover {
background: var(--accent-hover, #06ad56);
border-color: var(--accent-hover, #06ad56);
}
/* 输入区域 */
.input-box__area {
display: flex;
+47
View File
@@ -73,6 +73,12 @@ export const useConversationStore = defineStore('conversation', () => {
/** 审批流程链接列表 */
const approvalLinks = ref<ApprovalLink[]>([])
/** 审批卡片弹窗是否显示(关键词触发) */
const approvalCardVisible = ref<boolean>(false)
/** 触发审批卡片的关键词文本 */
const approvalCardTriggerText = ref<string>('')
/** 软件下载列表 */
const softwareDownloads = ref<SoftwareDownload[]>([])
@@ -359,6 +365,13 @@ export const useConversationStore = defineStore('conversation', () => {
return
}
// 检查是否包含审批关键词,如果包含则先弹出卡片
const hasApprovalKeyword = checkApprovalKeywords(content)
if (hasApprovalKeyword) {
console.log('[Store] 检测到审批关键词,弹窗后仍发送消息')
// 审批弹窗显示,但不阻止消息发送
}
// ========================================================================
// 步骤1:乐观更新 - 立即添加临时消息到列表
// ========================================================================
@@ -597,6 +610,35 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
// 审批关键词列表(静态配置,后续可从API获取)
const APPROVAL_KEYWORDS = ['申请', '资源', '设备', '电脑', '笔记本']
/** 检查文本是否包含审批关键词,触发审批卡片弹窗 */
function checkApprovalKeywords(text: string): boolean {
const lowerText = text.toLowerCase()
const hasKeyword = APPROVAL_KEYWORDS.some((kw) => lowerText.includes(kw))
if (hasKeyword) {
approvalCardTriggerText.value = text
approvalCardVisible.value = true
console.log('[Store] 检测到审批关键词,触发卡片弹窗')
}
return hasKeyword
}
/** 关闭审批卡片弹窗 */
function closeApprovalCard(): void {
approvalCardVisible.value = false
approvalCardTriggerText.value = ''
}
/** 显示审批卡片弹窗(快捷按钮触发) */
function showApprovalCard(triggerText: string = ''): void {
approvalCardTriggerText.value = triggerText
approvalCardVisible.value = true
}
/**
* 加载软件下载列表
* 从后端获取所有可下载的软件列表
@@ -817,6 +859,8 @@ export const useConversationStore = defineStore('conversation', () => {
agentOnline,
assistantPanelVisible,
approvalLinks,
approvalCardVisible,
approvalCardTriggerText,
softwareDownloads,
lastMessageId,
initialized,
@@ -844,6 +888,9 @@ export const useConversationStore = defineStore('conversation', () => {
stopPolling,
shakeAgent,
fetchApprovalLinks,
checkApprovalKeywords,
closeApprovalCard,
showApprovalCard,
fetchSoftwareDownloads,
toggleAssistantPanel,
switchToConversation,