feat: 审批流程模块 (T审批A审批)
- 新增 backend/app/api/approval.py 审批API - 前端H5支持发起审批、审批操作 - 添加审批卡片弹窗组件 - 路由注册审批模块
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user