369 lines
12 KiB
Vue
369 lines
12 KiB
Vue
<template>
|
||
<PageCard>
|
||
<template #toolbar-left>
|
||
<el-input
|
||
v-model="searchKeyword"
|
||
placeholder="搜索主体名称 / 编码 / CorpID"
|
||
clearable
|
||
class="search-input"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
<el-button @click="handleSearch">查询</el-button>
|
||
</template>
|
||
<template #toolbar-right>
|
||
<el-button type="primary" @click="openCreateDialog">新增主体</el-button>
|
||
</template>
|
||
|
||
<el-table
|
||
:data="pagedSubjects"
|
||
class="sino-table"
|
||
@row-click="handleRowClick"
|
||
>
|
||
<el-table-column prop="subjectName" label="主体名称" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="channelType" label="渠道类型" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.channelType === 'wecom' ? 'primary' : 'info'" size="small">
|
||
{{ channelLabel(row.channelType) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="subjectCode" label="主体编码" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="corpId" label="CorpID" min-width="220" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="120">
|
||
<template #default="{ row }">
|
||
<div class="status-cell" @click.stop>
|
||
<el-switch
|
||
v-model="row.status"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
@change="(val: any) => handleStatusChange(row, val)"
|
||
/>
|
||
<span class="status-text">{{ row.status === 1 ? '启用' : '禁用' }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<template #pagination>
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:total="filteredSubjects.length"
|
||
layout="total, prev, pager, next"
|
||
background
|
||
/>
|
||
</template>
|
||
|
||
<!-- 新增:弹窗 -->
|
||
<el-dialog v-model="dialogVisible" title="新增渠道主体" width="600px" destroy-on-close>
|
||
<el-form :model="dialogForm" label-width="120px">
|
||
<el-form-item label="渠道">
|
||
<el-select v-model="dialogForm.channelType" placeholder="选择渠道" style="width: 100%">
|
||
<el-option label="企微" value="wecom" />
|
||
<el-option label="钉钉" value="dingtalk" />
|
||
<el-option label="飞书" value="feishu" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="主体编码">
|
||
<el-input v-model="dialogForm.subjectCode" placeholder="主体编码" />
|
||
</el-form-item>
|
||
<el-form-item label="主体名称">
|
||
<el-input v-model="dialogForm.subjectName" placeholder="主体名称" />
|
||
</el-form-item>
|
||
<el-form-item label="CorpID">
|
||
<el-input v-model="dialogForm.corpId" placeholder="CorpID" />
|
||
</el-form-item>
|
||
<el-form-item label="AgentID">
|
||
<el-input v-model="dialogForm.agentId" placeholder="AgentID" />
|
||
</el-form-item>
|
||
<el-form-item label="AgentSecret">
|
||
<el-input v-model="dialogForm.agentSecret" placeholder="AgentSecret" />
|
||
</el-form-item>
|
||
<el-form-item label="会话存档">
|
||
<el-switch v-model="dialogForm.sessionArchiveEnabled" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
<el-form-item label="会话存档私钥">
|
||
<el-input v-model="dialogForm.sessionArchivePrivateKey" type="textarea" :rows="3" placeholder="会话存档私钥" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 详情/编辑:右侧抽屉 -->
|
||
<el-drawer
|
||
v-model="drawerVisible"
|
||
:title="`渠道主体详情${currentSubject.subjectName ? ' - ' + currentSubject.subjectName : ''}`"
|
||
size="480px"
|
||
destroy-on-close
|
||
>
|
||
<el-form :model="dialogForm" label-width="120px">
|
||
<el-form-item label="渠道">
|
||
<el-select v-model="dialogForm.channelType" placeholder="选择渠道" style="width: 100%">
|
||
<el-option label="企微" value="wecom" />
|
||
<el-option label="钉钉" value="dingtalk" />
|
||
<el-option label="飞书" value="feishu" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="主体编码">
|
||
<el-input v-model="dialogForm.subjectCode" placeholder="主体编码" />
|
||
</el-form-item>
|
||
<el-form-item label="主体名称">
|
||
<el-input v-model="dialogForm.subjectName" placeholder="主体名称" />
|
||
</el-form-item>
|
||
<el-form-item label="CorpID">
|
||
<el-input v-model="dialogForm.corpId" placeholder="CorpID" />
|
||
</el-form-item>
|
||
<el-form-item label="AgentID">
|
||
<el-input v-model="dialogForm.agentId" placeholder="AgentID" />
|
||
</el-form-item>
|
||
<el-form-item label="AgentSecret">
|
||
<el-input v-model="dialogForm.agentSecret" placeholder="AgentSecret" />
|
||
</el-form-item>
|
||
<el-form-item label="会话存档">
|
||
<el-switch v-model="dialogForm.sessionArchiveEnabled" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
<el-form-item label="会话存档私钥">
|
||
<el-input v-model="dialogForm.sessionArchivePrivateKey" type="textarea" :rows="3" placeholder="会话存档私钥" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="drawer-footer">
|
||
<div class="drawer-actions-left">
|
||
<el-button v-if="currentSubject.channelType === 'wecom'" type="warning" plain @click="handleSyncEnterpriseContacts(currentSubject)">同步企业通讯录</el-button>
|
||
<el-button type="danger" plain @click="handleDrawerDelete">删除</el-button>
|
||
</div>
|
||
<div class="drawer-actions">
|
||
<el-button @click="drawerVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
</PageCard>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
createChannelSubject,
|
||
listChannelSubjects,
|
||
updateChannelSubject,
|
||
deleteChannelSubject,
|
||
updateChannelSubjectStatus,
|
||
} from '@/api/channelSubject'
|
||
import { syncSubjectEnterpriseContacts } from '@/api/syncTask'
|
||
import { getSyncTask } from '@/api/syncTask'
|
||
import PageCard from '@/components/PageCard.vue'
|
||
|
||
const subjects = ref<any[]>([])
|
||
const dialogVisible = ref(false)
|
||
const isEdit = ref(false)
|
||
const editingId = ref<number | null>(null)
|
||
|
||
const dialogForm = reactive({
|
||
channelType: 'wecom',
|
||
subjectCode: '',
|
||
subjectName: '',
|
||
corpId: '',
|
||
agentId: '',
|
||
agentSecret: '',
|
||
sessionArchiveEnabled: 0,
|
||
sessionArchivePrivateKey: '',
|
||
})
|
||
|
||
const drawerVisible = ref(false)
|
||
const currentSubject = ref<any>({})
|
||
|
||
const searchKeyword = ref('')
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(10)
|
||
|
||
const filteredSubjects = computed(() => {
|
||
const kw = searchKeyword.value.trim().toLowerCase()
|
||
if (!kw) return subjects.value
|
||
return subjects.value.filter(
|
||
(s) =>
|
||
(s.subjectName || '').toLowerCase().includes(kw) ||
|
||
(s.subjectCode || '').toLowerCase().includes(kw) ||
|
||
(s.corpId || '').toLowerCase().includes(kw)
|
||
)
|
||
})
|
||
|
||
const pagedSubjects = computed(() => {
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
return filteredSubjects.value.slice(start, start + pageSize.value)
|
||
})
|
||
|
||
function handleSearch() {
|
||
currentPage.value = 1
|
||
}
|
||
|
||
function resetDialogForm() {
|
||
dialogForm.channelType = 'wecom'
|
||
dialogForm.subjectCode = ''
|
||
dialogForm.subjectName = ''
|
||
dialogForm.corpId = ''
|
||
dialogForm.agentId = ''
|
||
dialogForm.agentSecret = ''
|
||
dialogForm.sessionArchiveEnabled = 0
|
||
dialogForm.sessionArchivePrivateKey = ''
|
||
}
|
||
|
||
function fillDialogForm(row: any) {
|
||
dialogForm.channelType = row.channelType ?? 'wecom'
|
||
dialogForm.subjectCode = row.subjectCode ?? ''
|
||
dialogForm.subjectName = row.subjectName ?? ''
|
||
dialogForm.corpId = row.corpId ?? ''
|
||
dialogForm.agentId = row.agentId ?? ''
|
||
dialogForm.agentSecret = row.agentSecret ?? ''
|
||
dialogForm.sessionArchiveEnabled = row.sessionArchiveEnabled ?? 0
|
||
dialogForm.sessionArchivePrivateKey = row.sessionArchivePrivateKey ?? ''
|
||
}
|
||
|
||
function openCreateDialog() {
|
||
isEdit.value = false
|
||
editingId.value = null
|
||
resetDialogForm()
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function handleRowClick(row: any) {
|
||
isEdit.value = true
|
||
editingId.value = row.id
|
||
currentSubject.value = { ...row }
|
||
fillDialogForm(row)
|
||
drawerVisible.value = true
|
||
}
|
||
|
||
async function loadSubjects() {
|
||
try {
|
||
const res: any = await listChannelSubjects()
|
||
subjects.value = res.data || []
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message)
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
try {
|
||
if (isEdit.value && editingId.value != null) {
|
||
await updateChannelSubject(editingId.value, dialogForm)
|
||
ElMessage.success('更新成功')
|
||
} else {
|
||
await createChannelSubject(dialogForm)
|
||
ElMessage.success('创建成功')
|
||
}
|
||
dialogVisible.value = false
|
||
drawerVisible.value = false
|
||
await loadSubjects()
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message)
|
||
}
|
||
}
|
||
|
||
async function handleDelete(row: any) {
|
||
try {
|
||
await ElMessageBox.confirm('确认删除该渠道主体?', '提示', { type: 'warning' })
|
||
await deleteChannelSubject(row.id)
|
||
ElMessage.success('删除成功')
|
||
await loadSubjects()
|
||
} catch (e: any) {
|
||
if (e !== 'cancel') {
|
||
ElMessage.error(e.message)
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleDrawerDelete() {
|
||
if (!currentSubject.value?.id) return
|
||
await handleDelete(currentSubject.value)
|
||
drawerVisible.value = false
|
||
}
|
||
|
||
async function handleStatusChange(row: any, val: any) {
|
||
try {
|
||
await updateChannelSubjectStatus(row.id, { status: val })
|
||
ElMessage.success('状态更新成功')
|
||
await loadSubjects()
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message)
|
||
await loadSubjects()
|
||
}
|
||
}
|
||
|
||
async function pollTask(taskId: number, label: string) {
|
||
const maxAttempts = 60
|
||
for (let i = 0; i < maxAttempts; i++) {
|
||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||
try {
|
||
const res: any = await getSyncTask(taskId)
|
||
const task = res.data
|
||
if (!task) continue
|
||
if (task.taskStatus === 2) {
|
||
ElMessage.success(`${label}同步完成,共 ${task.syncedCount ?? 0} 条`)
|
||
return
|
||
}
|
||
if (task.taskStatus === 3) {
|
||
ElMessage.error(`${label}同步失败:${task.errorMessage || '未知错误'}`)
|
||
return
|
||
}
|
||
} catch (e: any) {
|
||
// ignore polling error
|
||
}
|
||
}
|
||
ElMessage.warning(`${label}同步时间较长,请稍后到列表查看`)
|
||
}
|
||
|
||
async function handleSyncEnterpriseContacts(row: any) {
|
||
try {
|
||
const res: any = await syncSubjectEnterpriseContacts(row.id)
|
||
const taskId = res.data?.id
|
||
if (taskId) {
|
||
ElMessage.info('企业通讯录同步任务已提交')
|
||
await pollTask(taskId, '企业通讯录')
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error(e.message)
|
||
}
|
||
}
|
||
|
||
function channelLabel(type: string) {
|
||
const map: Record<string, string> = {
|
||
wecom: '企微',
|
||
dingtalk: '钉钉',
|
||
feishu: '飞书',
|
||
}
|
||
return map[type] || type
|
||
}
|
||
|
||
function mask(value: string) {
|
||
if (!value) return '-'
|
||
if (value.length <= 4) return '****'
|
||
return value.slice(0, 2) + '****' + value.slice(-2)
|
||
}
|
||
|
||
onMounted(loadSubjects)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.search-input {
|
||
width: 280px;
|
||
}
|
||
|
||
.drawer-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.drawer-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
</style>
|