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
@@ -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;