This commit is contained in:
@@ -44,16 +44,13 @@
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm label-width="120px">
|
||||
<ElFormItem label="配置 Key">
|
||||
<ElInput :model-value="currentConfig?.config_key || '-'" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置说明">
|
||||
<span>{{ currentConfig?.description || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置值">
|
||||
<ElCheckboxGroup v-if="isPaymentConfig" v-model="editPaymentMethods">
|
||||
<ElCheckbox v-for="method in paymentMethods" :key="method" :label="method">
|
||||
{{ method }}
|
||||
{{ paymentMethodLabels[method] }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
<ElSwitch v-else-if="isBooleanConfig" v-model="editBoolean" />
|
||||
@@ -89,7 +86,6 @@
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div v-if="valueHint" class="config-hint">{{ valueHint }}</div>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
@@ -103,6 +99,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { SystemConfigService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
@@ -120,6 +117,7 @@
|
||||
|
||||
defineOptions({ name: 'SystemConfigs' })
|
||||
const { hasAuth } = useAuth()
|
||||
const route = useRoute()
|
||||
|
||||
const moduleOptions = [
|
||||
{ label: '运营商回调配置', value: 'carrier_callback' },
|
||||
@@ -149,14 +147,18 @@
|
||||
const editNumber = ref<number | null>(null)
|
||||
const editPaymentMethods = ref<AllowedPaymentMethod[]>([])
|
||||
const paymentMethods: AllowedPaymentMethod[] = ['wallet', 'wechat', 'alipay']
|
||||
const paymentMethodLabels: Record<AllowedPaymentMethod, string> = {
|
||||
wallet: '钱包',
|
||||
wechat: '微信',
|
||||
alipay: '支付宝'
|
||||
}
|
||||
const targetConfigHandled = ref(false)
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '配置 Key', prop: 'config_key' },
|
||||
{ label: '模块', prop: 'module' },
|
||||
{ label: '配置说明', prop: 'description' },
|
||||
{ label: '配置值', prop: 'value' },
|
||||
{ label: '控件', prop: 'control' },
|
||||
{ label: '注册状态', prop: 'registered' },
|
||||
{ label: '只读', prop: 'readonly' },
|
||||
{ label: '敏感', prop: 'sensitive' },
|
||||
@@ -167,7 +169,6 @@
|
||||
moduleOptions.find((item) => item.value === module)?.label || module
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ prop: 'config_key', label: '配置 Key', minWidth: 220, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'module',
|
||||
label: '模块',
|
||||
@@ -178,10 +179,10 @@
|
||||
{
|
||||
prop: 'value',
|
||||
label: '配置值',
|
||||
minWidth: 150,
|
||||
formatter: (row: SystemConfigItem) => row.value || '-'
|
||||
minWidth: 260,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: SystemConfigItem) => formatConfigValue(row)
|
||||
},
|
||||
{ prop: 'control', label: '控件', width: 100 },
|
||||
{
|
||||
prop: 'registered',
|
||||
label: '注册状态',
|
||||
@@ -216,7 +217,8 @@
|
||||
)
|
||||
const isPaymentConfig = computed(() =>
|
||||
[PAYMENT_CONFIG_KEYS.card, PAYMENT_CONFIG_KEYS.device].includes(
|
||||
currentConfig.value?.config_key as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
||||
currentConfig.value
|
||||
?.config_key as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
||||
)
|
||||
)
|
||||
const isIntegerConfig = computed(
|
||||
@@ -233,14 +235,18 @@
|
||||
!isBooleanConfig.value && !isIntegerConfig.value && currentConfig.value?.value_type === 'json'
|
||||
)
|
||||
|
||||
const valueHint = computed(() => {
|
||||
if (!currentConfig.value) return ''
|
||||
const { min, max } = currentConfig.value
|
||||
if (min !== null && max !== null) return `取值范围:${min}~${max}`
|
||||
if (min !== null) return `最小值:${min}`
|
||||
if (max !== null) return `最大值:${max}`
|
||||
return ''
|
||||
})
|
||||
const isPaymentConfigKey = (configKey: string) =>
|
||||
Object.values(PAYMENT_CONFIG_KEYS).includes(
|
||||
configKey as (typeof PAYMENT_CONFIG_KEYS)[keyof typeof PAYMENT_CONFIG_KEYS]
|
||||
)
|
||||
|
||||
const formatConfigValue = (config: SystemConfigItem) => {
|
||||
if (isPaymentConfigKey(config.config_key)) {
|
||||
const methods = parsePaymentMethods(config.value)
|
||||
return methods.length ? methods.map((method) => paymentMethodLabels[method]).join('、') : '-'
|
||||
}
|
||||
return config.value || '-'
|
||||
}
|
||||
|
||||
const getSubmittedValue = () => {
|
||||
if (isPaymentConfig.value) return JSON.stringify(editPaymentMethods.value)
|
||||
@@ -293,6 +299,12 @@
|
||||
if (res.code === 0) {
|
||||
configList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
const targetKey = String(route.query.config_key || '')
|
||||
const targetConfig = configList.value.find((config) => config.config_key === targetKey)
|
||||
if (targetKey && targetConfig && !targetConfigHandled.value) {
|
||||
targetConfigHandled.value = true
|
||||
showEditDialog(targetConfig)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
@@ -370,10 +382,5 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-configs-page {
|
||||
.config-hint {
|
||||
margin: -12px 0 0 120px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑企微应用' : '新增企微应用'"
|
||||
width="50%"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<div class="dialog-description">应用凭据由平台管理员维护,保存后可在应用列表测试连接。</div>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px" class="application-form">
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="企业 ID" prop="corp_id">
|
||||
<ElInput v-model="form.corp_id" placeholder="请输入企业微信 CorpID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="AgentID" prop="agent_id">
|
||||
<ElInputNumber
|
||||
v-model="form.agent_id"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="full-width"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="应用名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入应用名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="Secret" prop="secret">
|
||||
<ElInput
|
||||
v-model="form.secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入应用 Secret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDivider content-position="left">回调配置</ElDivider>
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="回调 Token" prop="callback_token">
|
||||
<ElInput
|
||||
v-model="form.callback_token"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="vertical"
|
||||
placeholder="请输入回调 Token"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<ElInput
|
||||
v-model="form.encoding_aes_key"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
resize="vertical"
|
||||
placeholder="请输入 43 位 EncodingAESKey"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="启用状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="禁用" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="visible = false">取消</ElButton>
|
||||
<ElButton
|
||||
v-if="isEdit"
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
:loading="testing"
|
||||
@click="testApplication"
|
||||
>
|
||||
测试连接
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="saveApplication"
|
||||
>
|
||||
保存应用
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { WecomService } from '@/api/modules'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import type { WecomApplication } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'WecomApplicationFormDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
application: WecomApplication | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'success'): void
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
const isEdit = computed(() => Boolean(props.application?.id))
|
||||
const formRef = ref<FormInstance>()
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
const form = reactive({
|
||||
corp_id: '',
|
||||
agent_id: 0,
|
||||
name: '',
|
||||
secret: '',
|
||||
callback_token: '',
|
||||
encoding_aes_key: '',
|
||||
enabled: true
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
corp_id: [{ required: true, message: '请输入企业 ID', trigger: 'blur' }],
|
||||
agent_id: [{ required: true, message: '请输入 AgentID', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||
secret: [{ required: true, message: '请输入 Secret', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const fillForm = (application: WecomApplication | null) => {
|
||||
Object.assign(
|
||||
form,
|
||||
application
|
||||
? {
|
||||
corp_id: application.corp_id,
|
||||
agent_id: application.agent_id,
|
||||
name: application.name,
|
||||
secret: application.secret || '',
|
||||
callback_token: application.callback_token || '',
|
||||
encoding_aes_key: application.encoding_aes_key || '',
|
||||
enabled: application.status === 1
|
||||
}
|
||||
: {
|
||||
corp_id: '',
|
||||
agent_id: 0,
|
||||
name: '',
|
||||
secret: '',
|
||||
callback_token: '',
|
||||
encoding_aes_key: '',
|
||||
enabled: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
fillForm(null)
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const saveApplication = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await WecomService.saveApplication({
|
||||
corp_id: form.corp_id,
|
||||
agent_id: form.agent_id,
|
||||
name: form.name,
|
||||
secret: form.secret,
|
||||
callback_token: form.callback_token,
|
||||
encoding_aes_key: form.encoding_aes_key,
|
||||
status: form.enabled ? 1 : 0
|
||||
})
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('企微应用保存成功')
|
||||
emit('success')
|
||||
visible.value = false
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testApplication = async () => {
|
||||
if (!props.application?.id) return
|
||||
|
||||
testing.value = true
|
||||
try {
|
||||
const response = await WecomService.testApplication(props.application.id)
|
||||
if (response.code === 0) {
|
||||
if (response.data.success) ElMessage.success('连接成功,已取得 access_token')
|
||||
else ElMessage.warning('连接失败,请检查应用凭据')
|
||||
}
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.application] as const,
|
||||
([opened]) => {
|
||||
if (opened) fillForm(props.application)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-description {
|
||||
margin-bottom: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.application-form {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<div class="wecom-application-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">{{ isEdit ? '编辑企微应用' : '新增企微应用' }}</div>
|
||||
<div class="page-description"
|
||||
>应用凭据由平台管理员维护,保存后可在应用列表测试连接。</div
|
||||
>
|
||||
</div>
|
||||
<ElButton @click="goBack">返回应用列表</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="140px"
|
||||
class="application-form"
|
||||
>
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="企业 ID" prop="corp_id">
|
||||
<ElInput v-model="form.corp_id" placeholder="请输入企业微信 CorpID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="AgentID" prop="agent_id">
|
||||
<ElInputNumber
|
||||
v-model="form.agent_id"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
class="full-width"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="应用名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入应用名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="Secret" prop="secret">
|
||||
<ElInput
|
||||
v-model="form.secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入应用 Secret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDivider content-position="left">回调配置</ElDivider>
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="回调 Token" prop="callback_token">
|
||||
<ElInput v-model="form.callback_token" placeholder="请输入回调 Token" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<ElInput v-model="form.encoding_aes_key" placeholder="请输入 43 位 EncodingAESKey" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="启用状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="禁用" />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="form-actions">
|
||||
<ElButton @click="goBack">取消</ElButton>
|
||||
<ElButton
|
||||
v-if="isEdit"
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
:loading="testing"
|
||||
@click="testApplication"
|
||||
>
|
||||
测试连接
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="saveApplication"
|
||||
>
|
||||
保存应用
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { WecomService } from '@/api/modules'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import type { WecomApplication } from '@/types/api'
|
||||
|
||||
defineOptions({ name: 'WecomApplicationDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
const saving = ref(false)
|
||||
const testing = ref(false)
|
||||
const applicationId = computed(() => Number(route.params.id) || 0)
|
||||
const isEdit = computed(() => applicationId.value > 0)
|
||||
const form = reactive({
|
||||
corp_id: '',
|
||||
agent_id: 0,
|
||||
name: '',
|
||||
secret: '',
|
||||
callback_token: '',
|
||||
encoding_aes_key: '',
|
||||
enabled: true
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
corp_id: [{ required: true, message: '请输入企业 ID', trigger: 'blur' }],
|
||||
agent_id: [{ required: true, message: '请输入 AgentID', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||
secret: [{ required: true, message: '请输入 Secret', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const fillForm = (application?: WecomApplication) => {
|
||||
Object.assign(
|
||||
form,
|
||||
application
|
||||
? {
|
||||
corp_id: application.corp_id,
|
||||
agent_id: application.agent_id,
|
||||
name: application.name,
|
||||
secret: application.secret || '',
|
||||
callback_token: application.callback_token || '',
|
||||
encoding_aes_key: application.encoding_aes_key || '',
|
||||
enabled: application.status === 1
|
||||
}
|
||||
: {
|
||||
corp_id: '',
|
||||
agent_id: 0,
|
||||
name: '',
|
||||
secret: '',
|
||||
callback_token: '',
|
||||
encoding_aes_key: '',
|
||||
enabled: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const loadApplication = async () => {
|
||||
if (!isEdit.value) {
|
||||
fillForm()
|
||||
return
|
||||
}
|
||||
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
||||
if (response.code === 0) {
|
||||
const application = response.data.items?.find((item) => item.id === applicationId.value)
|
||||
if (application) fillForm(application)
|
||||
else {
|
||||
ElMessage.error('未找到对应的企微应用')
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveApplication = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await WecomService.saveApplication({
|
||||
corp_id: form.corp_id,
|
||||
agent_id: form.agent_id,
|
||||
name: form.name,
|
||||
secret: form.secret,
|
||||
callback_token: form.callback_token,
|
||||
encoding_aes_key: form.encoding_aes_key,
|
||||
status: form.enabled ? 1 : 0
|
||||
})
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('企微应用保存成功')
|
||||
await router.push(RoutesAlias.WecomApplications)
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testApplication = async () => {
|
||||
if (!applicationId.value) return
|
||||
testing.value = true
|
||||
try {
|
||||
const response = await WecomService.testApplication(applicationId.value)
|
||||
if (response.code === 0) {
|
||||
if (response.data.success) ElMessage.success('连接成功,已取得 access_token')
|
||||
else ElMessage.warning('连接失败,请检查应用凭据')
|
||||
}
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => void router.push(RoutesAlias.WecomApplications)
|
||||
|
||||
onMounted(() => void loadApplication())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wecom-application-detail-page {
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.application-form {
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +1,128 @@
|
||||
<template>
|
||||
<div class="wecom-applications-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">企微应用管理</div>
|
||||
<div class="page-description">管理企业微信应用凭据,并查看连接和默认发起人状态。</div>
|
||||
</div>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
type="primary"
|
||||
@click="goToCreate"
|
||||
>
|
||||
新增应用
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable v-loading="loading" :data="applications" border>
|
||||
<ElTableColumn prop="name" label="应用名称" min-width="160" show-overflow-tooltip />
|
||||
<ElTableColumn label="凭据状态" width="110">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.credentials_set ? 'success' : 'warning'" size="small">
|
||||
{{ scope.row.credentials_set ? '已完整' : '未完整' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="启用状态" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.status_name }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="默认发起人" min-width="150">
|
||||
<template #default="scope">{{ scope.row.default_creator_name || '未设置' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="最近连接" min-width="170">
|
||||
<template #default="scope">{{ formatDate(scope.row.last_connected_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="300" fixed="right">
|
||||
<template #default="scope">
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="wecom-applications-page">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadApplications"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
link
|
||||
type="primary"
|
||||
@click="testApplication(scope.row.id)"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
测试连接
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||
link
|
||||
type="primary"
|
||||
@click="goToMembers(scope.row.id)"
|
||||
>
|
||||
成员管理
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.application"
|
||||
link
|
||||
@click="goToDetail(scope.row.id)"
|
||||
>
|
||||
编辑配置
|
||||
新增应用
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ArtTableHeader>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="applications"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="160"
|
||||
:inline-actions-count="1"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="loadApplications"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<WecomApplicationFormDialog
|
||||
v-model="applicationDialogVisible"
|
||||
:application="editingApplication"
|
||||
@success="handleApplicationSaved"
|
||||
/>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { WecomService } from '@/api/modules'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import type { WecomApplication } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import WecomApplicationFormDialog from './components/WecomApplicationFormDialog.vue'
|
||||
|
||||
defineOptions({ name: 'WecomApplications' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const tableRef = ref()
|
||||
const loading = ref(false)
|
||||
const applications = ref<WecomApplication[]>([])
|
||||
const applicationDialogVisible = ref(false)
|
||||
const editingApplication = ref<WecomApplication | null>(null)
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '应用名称', prop: 'name' },
|
||||
{ label: '凭据状态', prop: 'credentials_set' },
|
||||
{ label: '启用状态', prop: 'status' },
|
||||
{ label: '默认发起人', prop: 'default_creator_name' },
|
||||
{ label: '最近连接', prop: 'last_connected_at' }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '应用名称',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'credentials_set',
|
||||
label: '凭据状态',
|
||||
width: 110,
|
||||
formatter: (row: WecomApplication) =>
|
||||
h(ElTag, { type: row.credentials_set ? 'success' : 'warning', size: 'small' }, () =>
|
||||
row.credentials_set ? '已完整' : '未完整'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '启用状态',
|
||||
width: 100,
|
||||
formatter: (row: WecomApplication) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.status === 1 ? 'success' : 'info', size: 'small' },
|
||||
() => row.status_name
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'default_creator_name',
|
||||
label: '默认发起人',
|
||||
minWidth: 150,
|
||||
formatter: (row: WecomApplication) => row.default_creator_name || '未设置'
|
||||
},
|
||||
{
|
||||
prop: 'last_connected_at',
|
||||
label: '最近连接',
|
||||
width: 180,
|
||||
formatter: (row: WecomApplication) => formatDate(row.last_connected_at)
|
||||
}
|
||||
])
|
||||
|
||||
const loadApplications = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -124,8 +145,20 @@
|
||||
void loadApplications()
|
||||
}
|
||||
|
||||
const goToCreate = () => void router.push(`${RoutesAlias.WecomApplications}/create`)
|
||||
const goToDetail = (id: number) => void router.push(`${RoutesAlias.WecomApplicationDetail}/${id}`)
|
||||
const openCreateDialog = () => {
|
||||
editingApplication.value = null
|
||||
applicationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditDialog = (application: WecomApplication) => {
|
||||
editingApplication.value = application
|
||||
applicationDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleApplicationSaved = () => {
|
||||
void loadApplications()
|
||||
}
|
||||
|
||||
const goToMembers = (id: number) =>
|
||||
void router.push({ path: RoutesAlias.WecomMembers, query: { application_id: String(id) } })
|
||||
|
||||
@@ -138,34 +171,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: WecomApplication) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth(JULY_PERMISSIONS.wecom.application)) {
|
||||
actions.push({
|
||||
label: '测试连接',
|
||||
handler: () => testApplication(row.id),
|
||||
type: 'primary'
|
||||
})
|
||||
actions.push({
|
||||
label: '编辑配置',
|
||||
handler: () => openEditDialog(row),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
if (hasAuth(JULY_PERMISSIONS.wecom.member)) {
|
||||
actions.push({
|
||||
label: '成员管理',
|
||||
handler: () => goToMembers(row.id),
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => void loadApplications())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wecom-applications-page {
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,167 +1,94 @@
|
||||
<template>
|
||||
<div class="wecom-members-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">企微成员管理</div>
|
||||
<div class="page-description"
|
||||
>同步应用可见成员,设置默认审批发起人,并完成平台账号绑定。</div
|
||||
>
|
||||
</div>
|
||||
<ElButton @click="goToApplications">返回应用列表</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="wecom-members-page">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElForm inline class="search-form" @submit.prevent>
|
||||
<ElFormItem label="企微应用">
|
||||
<ElSelect
|
||||
v-model="selectedApplicationId"
|
||||
filterable
|
||||
class="application-select"
|
||||
placeholder="请选择企微应用"
|
||||
@change="handleApplicationChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
:label="application.name"
|
||||
:value="application.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="成员搜索">
|
||||
<ElInput
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="姓名"
|
||||
class="keyword-input"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||
<ElButton @click="handleReset">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="selected-application">
|
||||
当前应用:<span>{{ selectedApplication?.name || '未选择' }}</span>
|
||||
<ElTag v-if="selectedApplication?.default_creator_name" type="success" size="small">
|
||||
默认发起人:{{ selectedApplication.default_creator_name }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||
:disabled="!selectedApplicationId"
|
||||
:loading="syncing"
|
||||
@click="syncMembers"
|
||||
>
|
||||
同步成员
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable
|
||||
v-loading="loading"
|
||||
:data="members"
|
||||
row-key="userid"
|
||||
highlight-current-row
|
||||
border
|
||||
@row-click="selectMember"
|
||||
>
|
||||
<ElTableColumn label="选择" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<ElRadio v-model="selectedUserid" :label="scope.row.userid">
|
||||
<span class="sr-only">选择 {{ scope.row.name }}</span>
|
||||
</ElRadio>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="name" label="成员姓名" min-width="150" />
|
||||
<ElTableColumn prop="synced_at" label="同步时间" min-width="180" />
|
||||
<ElTableColumn label="操作" width="230" fixed="right">
|
||||
<template #default="scope">
|
||||
<ElCard v-loading="loading" shadow="never" class="art-table-card members-card">
|
||||
<div class="members-toolbar">
|
||||
<div class="toolbar-actions">
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||
link
|
||||
:disabled="!selectedApplicationId"
|
||||
:loading="syncing"
|
||||
type="primary"
|
||||
@click.stop="setDefaultCreator(scope.row)"
|
||||
@click="syncMembers"
|
||||
>
|
||||
设为默认发起人
|
||||
同步成员
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.binding"
|
||||
link
|
||||
@click.stop="selectMember(scope.row)"
|
||||
>
|
||||
绑定账号
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElButton @click="goToApplications">返回应用列表</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="loadMembers"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElEmpty v-if="!members.length && !loading" description="暂无企微成员" />
|
||||
<div v-else class="member-grid">
|
||||
<ElCard v-for="member in members" :key="member.userid" shadow="hover" class="member-card">
|
||||
<div class="member-card__header">
|
||||
<div class="member-card__identity">
|
||||
<ElAvatar :size="36" class="member-card__avatar">
|
||||
{{ getMemberInitial(member.name) }}
|
||||
</ElAvatar>
|
||||
<div class="member-card__name" :title="member.name">
|
||||
{{ member.name || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<ElTag v-if="isDefaultCreator(member)" type="success" size="small">
|
||||
默认发起人
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-else
|
||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||
class="member-card__action-tag"
|
||||
type="primary"
|
||||
effect="plain"
|
||||
size="small"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.stop="setDefaultCreator(member)"
|
||||
@keydown.enter.stop="setDefaultCreator(member)"
|
||||
>
|
||||
设为默认发起人
|
||||
</ElTag>
|
||||
</div>
|
||||
|
||||
<ElCard shadow="never" class="operation-card">
|
||||
<template #header>当前成员操作</template>
|
||||
<ElAlert
|
||||
v-if="selectedMember"
|
||||
:title="`已选择:${selectedMember.name}`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
<ElEmpty v-else description="请先在上方列表选择成员" :image-size="70" />
|
||||
<div class="member-card__info">
|
||||
<span class="member-card__label">同步时间</span>
|
||||
<span>{{ formatDateTime(member.synced_at) }}</span>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="operation-row">
|
||||
<span class="operation-label">绑定平台账号</span>
|
||||
<ElInputNumber
|
||||
v-model="bindingAccountId"
|
||||
:min="1"
|
||||
:disabled="!selectedMember"
|
||||
controls-position="right"
|
||||
placeholder="账号 ID"
|
||||
/>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.binding"
|
||||
type="primary"
|
||||
:disabled="!selectedMember || !bindingAccountId"
|
||||
:loading="binding"
|
||||
@click="bindAccount"
|
||||
>
|
||||
绑定账号
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.member"
|
||||
:disabled="!selectedMember"
|
||||
:loading="savingDefault"
|
||||
@click="saveSelectedDefaultCreator"
|
||||
>
|
||||
保存为默认发起人
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="loadMembers"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { AccountService, WecomService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { WecomService } from '@/api/modules'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import type { WecomApplication, WecomMember } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'WecomMembers' })
|
||||
|
||||
@@ -170,30 +97,65 @@
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const savingDefault = ref(false)
|
||||
const binding = ref(false)
|
||||
const pendingDefaultUserid = ref('')
|
||||
const applications = ref<WecomApplication[]>([])
|
||||
const members = ref<WecomMember[]>([])
|
||||
const selectedApplicationId = ref<number>()
|
||||
const selectedUserid = ref('')
|
||||
const bindingAccountId = ref<number>()
|
||||
const keyword = ref('')
|
||||
const searchForm = reactive({
|
||||
application_id: undefined as number | undefined,
|
||||
keyword: ''
|
||||
})
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const selectedApplicationId = computed(() => searchForm.application_id)
|
||||
const selectedApplication = computed(() =>
|
||||
applications.value.find((application) => application.id === selectedApplicationId.value)
|
||||
)
|
||||
const selectedMember = computed(() =>
|
||||
members.value.find((member) => member.userid === selectedUserid.value)
|
||||
)
|
||||
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '企微应用',
|
||||
prop: 'application_id',
|
||||
type: 'select',
|
||||
options: applications.value.map((application) => ({
|
||||
label: application.name,
|
||||
value: application.id
|
||||
})),
|
||||
onChange: () => {
|
||||
pagination.page = 1
|
||||
void loadMembers()
|
||||
},
|
||||
config: {
|
||||
filterable: true,
|
||||
clearable: false,
|
||||
placeholder: '请选择企微应用'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '成员搜索',
|
||||
prop: 'keyword',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入成员姓名'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const isDefaultCreator = (member: WecomMember) =>
|
||||
member.userid === selectedApplication.value?.default_creator_userid
|
||||
|
||||
const getMemberInitial = (name: string) => name.trim().slice(0, 1) || '-'
|
||||
|
||||
const loadApplications = async () => {
|
||||
const response = await WecomService.getApplications({ page: 1, page_size: 100 })
|
||||
if (response.code !== 0) return
|
||||
applications.value = response.data.items || []
|
||||
const queryApplicationId = Number(route.query.application_id)
|
||||
selectedApplicationId.value =
|
||||
applications.value.find((application) => application.id === queryApplicationId)?.id ||
|
||||
applications.value[0]?.id
|
||||
const preferredApplication =
|
||||
applications.value.find((application) => application.id === queryApplicationId) ||
|
||||
applications.value.find((application) => application.id === searchForm.application_id) ||
|
||||
applications.value[0]
|
||||
searchForm.application_id = preferredApplication?.id
|
||||
}
|
||||
|
||||
const loadMembers = async () => {
|
||||
@@ -207,33 +169,25 @@
|
||||
const response = await WecomService.getMembers(selectedApplicationId.value, {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
keyword: keyword.value.trim() || undefined
|
||||
keyword: searchForm.keyword.trim() || undefined
|
||||
})
|
||||
if (response.code === 0) {
|
||||
members.value = response.data.items || []
|
||||
pagination.total = response.data.total || 0
|
||||
if (!members.value.some((member) => member.userid === selectedUserid.value)) {
|
||||
selectedUserid.value = ''
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplicationChange = () => {
|
||||
pagination.page = 1
|
||||
selectedUserid.value = ''
|
||||
void loadMembers()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
void loadMembers()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
keyword.value = ''
|
||||
searchForm.keyword = ''
|
||||
searchForm.application_id = applications.value[0]?.id
|
||||
pagination.page = 1
|
||||
void loadMembers()
|
||||
}
|
||||
@@ -244,10 +198,6 @@
|
||||
void loadMembers()
|
||||
}
|
||||
|
||||
const selectMember = (member: WecomMember) => {
|
||||
selectedUserid.value = member.userid
|
||||
}
|
||||
|
||||
const syncMembers = async () => {
|
||||
if (!selectedApplicationId.value) return
|
||||
syncing.value = true
|
||||
@@ -263,49 +213,40 @@
|
||||
}
|
||||
|
||||
const setDefaultCreator = async (member: WecomMember) => {
|
||||
if (!selectedApplicationId.value) return
|
||||
selectedUserid.value = member.userid
|
||||
await saveSelectedDefaultCreator()
|
||||
}
|
||||
if (!selectedApplicationId.value || savingDefault.value) return
|
||||
|
||||
const saveSelectedDefaultCreator = async () => {
|
||||
if (!selectedApplicationId.value || !selectedMember.value) {
|
||||
ElMessage.warning('请选择要设置的成员')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将 ${member.name} 设置为当前应用的默认发起人吗?`,
|
||||
'确认设置默认发起人',
|
||||
{
|
||||
confirmButtonText: '确定设置',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
savingDefault.value = true
|
||||
pendingDefaultUserid.value = member.userid
|
||||
try {
|
||||
const response = await WecomService.setDefaultCreator(
|
||||
selectedApplicationId.value,
|
||||
selectedMember.value.userid
|
||||
member.userid
|
||||
)
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('默认发起人已保存')
|
||||
const application = selectedApplication.value
|
||||
if (application) {
|
||||
application.default_creator_userid = selectedMember.value.userid
|
||||
application.default_creator_name = selectedMember.value.name
|
||||
application.default_creator_userid = member.userid
|
||||
application.default_creator_name = member.name
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
savingDefault.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const bindAccount = async () => {
|
||||
if (!selectedApplicationId.value || !selectedMember.value || !bindingAccountId.value) {
|
||||
ElMessage.warning('请选择成员并输入账号 ID')
|
||||
return
|
||||
}
|
||||
binding.value = true
|
||||
try {
|
||||
const response = await AccountService.bindWecom(bindingAccountId.value, {
|
||||
application_id: selectedApplicationId.value,
|
||||
userid: selectedMember.value.userid
|
||||
})
|
||||
if (response.code === 0) ElMessage.success('账号企微绑定成功')
|
||||
} finally {
|
||||
binding.value = false
|
||||
pendingDefaultUserid.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,105 +260,152 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wecom-members-page {
|
||||
.page-header,
|
||||
.toolbar,
|
||||
.operation-row {
|
||||
.members-card {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(> .el-card__body) {
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.members-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.member-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.member-card {
|
||||
min-width: 0;
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
height: auto;
|
||||
min-height: 108px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.member-card__header,
|
||||
.member-card__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.toolbar {
|
||||
.member-card__header {
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 16px;
|
||||
.member-card__identity {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.member-card__avatar {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
.member-card__name {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-card__info {
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
|
||||
span:last-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-top: 4px;
|
||||
padding: 14px 16px 0;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
.member-card__label {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.application-select {
|
||||
width: 280px;
|
||||
}
|
||||
.member-card__action-tag {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.keyword-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin: 20px 0 12px;
|
||||
|
||||
.selected-application {
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
span {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
margin-left: 10px;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.operation-card {
|
||||
margin-top: 16px;
|
||||
@media (max-width: 1400px) {
|
||||
.member-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.operation-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
@media (max-width: 1024px) {
|
||||
.member-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.operation-label {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
@media (max-width: 640px) {
|
||||
.member-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.application-select,
|
||||
.keyword-input {
|
||||
width: 100%;
|
||||
.members-toolbar {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,240 @@
|
||||
<template>
|
||||
<div class="wecom-scenes-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-title">企微审批场景</div>
|
||||
<div class="page-description"
|
||||
>分别维护退款和线下代充值审批模板,并提交后由后端校验控件映射。</div
|
||||
<ArtTableFullScreen>
|
||||
<div id="table-full-screen" class="wecom-scenes-page">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadData"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||
type="primary"
|
||||
@click="createScene"
|
||||
>
|
||||
新增场景配置
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="business_type"
|
||||
:loading="loading"
|
||||
:data="scenes"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:margin-top="10"
|
||||
:actions="getActions"
|
||||
:actions-width="140"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentPageChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="editorVisible"
|
||||
:title="editing ? '编辑审批场景' : '新增审批场景'"
|
||||
width="55%"
|
||||
class="wecom-scene-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm :model="sceneForm" label-width="80px" class="scene-editor-form">
|
||||
<div class="dialog-section">
|
||||
<div class="dialog-section__header">
|
||||
<div class="dialog-section__title">基础配置</div>
|
||||
<div class="dialog-section__description">选择审批业务和对应的企业微信模板</div>
|
||||
</div>
|
||||
<ElButton v-permission="JULY_PERMISSIONS.wecom.scene" type="primary" @click="createScene">
|
||||
新增场景配置
|
||||
|
||||
<ElRow :gutter="24">
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="业务类型" required>
|
||||
<ElSelect
|
||||
v-model="sceneForm.business_type"
|
||||
:disabled="editing"
|
||||
class="full-width"
|
||||
@change="handleBusinessTypeChange"
|
||||
>
|
||||
<ElOption label="退款审批" value="refund_approval" />
|
||||
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="企微应用">
|
||||
<ElSelect
|
||||
v-model="sceneForm.application_id"
|
||||
clearable
|
||||
filterable
|
||||
class="full-width"
|
||||
>
|
||||
<ElOption
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
:label="application.name"
|
||||
:value="application.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="模板 ID" required>
|
||||
<ElInput v-model="sceneForm.template_id" placeholder="请输入企微后台模板 ID">
|
||||
<template #append>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||
:loading="inspectingTemplate"
|
||||
@click="syncTemplate()"
|
||||
>
|
||||
同步模板
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
<div v-if="templateName" class="template-name">当前模板:{{ templateName }}</div>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12">
|
||||
<ElFormItem label="启用状态">
|
||||
<ElSwitch v-model="sceneForm.enabled" active-text="启用" inactive-text="禁用" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<div class="dialog-section mapping-section">
|
||||
<div class="dialog-section__header mapping-heading">
|
||||
<div>
|
||||
<div class="dialog-section__title">控件映射</div>
|
||||
<div class="dialog-section__description">将业务字段绑定到企业微信模板控件</div>
|
||||
</div>
|
||||
<ElTag v-if="!templateControls.length" type="info" effect="plain" class="mapping-tip">
|
||||
请先填写模板 ID 并点击“同步模板”,再选择模板控件。
|
||||
</ElTag>
|
||||
</div>
|
||||
<div v-if="mappingRows.length" class="mapping-list">
|
||||
<div v-for="mapping in mappingRows" :key="mapping.key" class="mapping-row">
|
||||
<div class="mapping-row__grid">
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">业务字段</span>
|
||||
<ElSelect
|
||||
v-model="mapping.business_field"
|
||||
filterable
|
||||
clearable
|
||||
:loading="fieldsLoading"
|
||||
placeholder="选择业务字段"
|
||||
>
|
||||
<ElOption
|
||||
v-for="field in businessFields"
|
||||
:key="field.code"
|
||||
:label="field.name"
|
||||
:value="field.code"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">模板控件</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_title"
|
||||
readonly
|
||||
placeholder="模板控件名称"
|
||||
/>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">控件 ID</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_id"
|
||||
readonly
|
||||
placeholder="选择模板控件后自动填充"
|
||||
/>
|
||||
</div>
|
||||
<div class="mapping-field">
|
||||
<span class="mapping-field__label">控件类型</span>
|
||||
<ElInput
|
||||
:model-value="mapping.control_type"
|
||||
readonly
|
||||
placeholder="同步模板后自动填充"
|
||||
/>
|
||||
</div>
|
||||
<div class="mapping-field mapping-field--option">
|
||||
<span class="mapping-field__label">选择项映射</span>
|
||||
<ElInput v-model="mapping.option_mapping" placeholder="JSON,可为空对象" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无控件映射,请新增一条" :image-size="60" />
|
||||
</div>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="editorVisible = false">取消</ElButton>
|
||||
<ElButton @click="createScene">清空表单</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="saveScene"
|
||||
>
|
||||
保存并校验
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="scene-layout">
|
||||
<div class="scene-list">
|
||||
<div class="section-heading">场景配置列表</div>
|
||||
<ElTable
|
||||
v-loading="loading"
|
||||
:data="scenes"
|
||||
row-key="business_type"
|
||||
highlight-current-row
|
||||
border
|
||||
@row-click="selectScene"
|
||||
>
|
||||
<ElTableColumn prop="business_type_name" label="业务类型" min-width="150" />
|
||||
<ElTableColumn
|
||||
prop="template_name"
|
||||
label="模板名称"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="状态" width="90">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.status_name }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="last_verified_at" label="最近校验" min-width="170" />
|
||||
<ElTableColumn label="操作" width="90" fixed="right">
|
||||
<template #default="scope">
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||
link
|
||||
type="primary"
|
||||
@click.stop="selectScene(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<ElCard shadow="never" class="editor-card">
|
||||
<template #header>
|
||||
<div class="editor-header">
|
||||
<span>{{ editing ? '编辑审批场景' : '新增审批场景' }}</span>
|
||||
<ElTag v-if="editing" size="small" type="info">{{ sceneForm.business_type }}</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElForm :model="sceneForm" label-width="110px">
|
||||
<ElFormItem label="业务类型" required>
|
||||
<ElSelect v-model="sceneForm.business_type" :disabled="editing" class="full-width">
|
||||
<ElOption label="退款审批" value="refund_approval" />
|
||||
<ElOption label="线下代充值审批" value="offline_recharge_approval" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="企微应用">
|
||||
<ElSelect v-model="sceneForm.application_id" clearable filterable class="full-width">
|
||||
<ElOption
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
:label="application.name"
|
||||
:value="application.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="模板 ID" required>
|
||||
<ElInput v-model="sceneForm.template_id" placeholder="请输入企微后台模板 ID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用状态">
|
||||
<ElSwitch v-model="sceneForm.enabled" active-text="启用" inactive-text="禁用" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElDivider content-position="left">控件映射</ElDivider>
|
||||
<div class="mapping-heading">
|
||||
<span>业务字段与模板控件的对应关系</span>
|
||||
<ElButton text type="primary" @click="addMapping()">新增映射</ElButton>
|
||||
</div>
|
||||
<div v-if="mappingRows.length" class="mapping-list">
|
||||
<div v-for="(mapping, index) in mappingRows" :key="mapping.key" class="mapping-row">
|
||||
<ElInput v-model="mapping.business_field" placeholder="业务字段,如 refund_no" />
|
||||
<ElInput v-model="mapping.control_id" placeholder="控件 ID" />
|
||||
<ElInput v-model="mapping.control_type" placeholder="控件类型,如 Text" />
|
||||
<ElInput
|
||||
v-model="mapping.option_mapping"
|
||||
placeholder="选择项映射 JSON,可为空对象"
|
||||
/>
|
||||
<ElButton text type="danger" @click="removeMapping(index)">删除</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无控件映射,请新增一条" :image-size="60" />
|
||||
|
||||
<div class="editor-actions">
|
||||
<ElButton @click="createScene">清空表单</ElButton>
|
||||
<ElButton
|
||||
v-permission="JULY_PERMISSIONS.wecom.scene"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="saveScene"
|
||||
>
|
||||
保存并校验
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { WecomService } from '@/api/modules'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import type {
|
||||
WecomApplication,
|
||||
WecomBusinessField,
|
||||
WecomBusinessType,
|
||||
WecomScene,
|
||||
WecomSceneControlMapping
|
||||
WecomSceneControlMapping,
|
||||
WecomTemplateControl
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'WecomScenes' })
|
||||
|
||||
interface MappingEditor {
|
||||
key: number
|
||||
business_field: string
|
||||
control_title: string
|
||||
control_id: string
|
||||
control_type: string
|
||||
required: boolean
|
||||
option_mapping: string
|
||||
}
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
const tableRef = ref()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const fieldsLoading = ref(false)
|
||||
const inspectingTemplate = ref(false)
|
||||
const editorVisible = ref(false)
|
||||
const applications = ref<WecomApplication[]>([])
|
||||
const businessFields = ref<WecomBusinessField[]>([])
|
||||
const templateControls = ref<WecomTemplateControl[]>([])
|
||||
const templateName = ref('')
|
||||
const scenes = ref<WecomScene[]>([])
|
||||
const selectedBusinessType = ref<WecomBusinessType>()
|
||||
const nextMappingKey = ref(1)
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
const sceneForm = reactive({
|
||||
business_type: 'refund_approval' as WecomBusinessType,
|
||||
application_id: undefined as number | undefined,
|
||||
@@ -162,19 +244,69 @@
|
||||
const mappingRows = ref<MappingEditor[]>([])
|
||||
const editing = computed(() => Boolean(selectedBusinessType.value))
|
||||
|
||||
const addMapping = (mapping?: Partial<MappingEditor>) => {
|
||||
mappingRows.value.push({
|
||||
key: nextMappingKey.value++,
|
||||
business_field: mapping?.business_field || '',
|
||||
control_id: mapping?.control_id || '',
|
||||
control_type: mapping?.control_type || '',
|
||||
option_mapping: mapping?.option_mapping || '{}'
|
||||
})
|
||||
const columnOptions = [
|
||||
{ label: '业务类型', prop: 'business_type_name' },
|
||||
{ label: '模板名称', prop: 'template_name' },
|
||||
{ label: '状态', prop: 'status' },
|
||||
{ label: '最近校验', prop: 'last_verified_at' }
|
||||
]
|
||||
|
||||
const formatDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
|
||||
|
||||
const loadBusinessFields = async (businessType: WecomBusinessType) => {
|
||||
fieldsLoading.value = true
|
||||
try {
|
||||
const response = await WecomService.getBusinessFields(businessType)
|
||||
if (response.code === 0) {
|
||||
businessFields.value = response.data.items || []
|
||||
}
|
||||
} finally {
|
||||
fieldsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const removeMapping = (index: number) => mappingRows.value.splice(index, 1)
|
||||
const handleBusinessTypeChange = () => {
|
||||
businessFields.value = []
|
||||
mappingRows.value.forEach((mapping) => {
|
||||
mapping.business_field = ''
|
||||
})
|
||||
void loadBusinessFields(sceneForm.business_type)
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'business_type_name',
|
||||
label: '业务类型',
|
||||
minWidth: 180
|
||||
},
|
||||
{
|
||||
prop: 'template_name',
|
||||
label: '模板名称',
|
||||
minWidth: 220,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
formatter: (row: WecomScene) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.status === 1 ? 'success' : 'info', size: 'small' },
|
||||
() => row.status_name
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'last_verified_at',
|
||||
label: '最近校验',
|
||||
width: 180,
|
||||
formatter: (row: WecomScene) => formatDate(row.last_verified_at)
|
||||
}
|
||||
])
|
||||
|
||||
const fillEditor = (scene?: WecomScene) => {
|
||||
templateControls.value = []
|
||||
templateName.value = ''
|
||||
if (!scene) {
|
||||
selectedBusinessType.value = undefined
|
||||
Object.assign(sceneForm, {
|
||||
@@ -184,7 +316,6 @@
|
||||
enabled: true
|
||||
})
|
||||
mappingRows.value = []
|
||||
addMapping()
|
||||
return
|
||||
}
|
||||
selectedBusinessType.value = scene.business_type
|
||||
@@ -197,41 +328,124 @@
|
||||
mappingRows.value = (scene.control_mapping || []).map((mapping) => ({
|
||||
key: nextMappingKey.value++,
|
||||
business_field: mapping.business_field,
|
||||
control_title: '',
|
||||
control_id: mapping.control_id,
|
||||
control_type: mapping.control_type,
|
||||
required: true,
|
||||
option_mapping: JSON.stringify(mapping.option_mapping || {})
|
||||
}))
|
||||
}
|
||||
|
||||
const selectScene = (scene: WecomScene) => fillEditor(scene)
|
||||
const createScene = () => fillEditor()
|
||||
const createScene = () => {
|
||||
fillEditor()
|
||||
editorVisible.value = true
|
||||
void loadBusinessFields(sceneForm.business_type)
|
||||
}
|
||||
|
||||
const editScene = (scene: WecomScene) => {
|
||||
fillEditor(scene)
|
||||
editorVisible.value = true
|
||||
void loadBusinessFields(scene.business_type)
|
||||
void syncTemplate(false)
|
||||
}
|
||||
|
||||
const syncMappingRows = (controls: WecomTemplateControl[]) => {
|
||||
const existingMappings = new Map(
|
||||
mappingRows.value.map((mapping) => [mapping.control_id, mapping])
|
||||
)
|
||||
mappingRows.value = controls.map((control) => {
|
||||
const existing = existingMappings.get(control.id)
|
||||
return {
|
||||
key: existing?.key || nextMappingKey.value++,
|
||||
business_field: existing?.business_field || '',
|
||||
control_title: control.title,
|
||||
control_id: control.id,
|
||||
control_type: control.type,
|
||||
required: control.required,
|
||||
option_mapping: existing?.option_mapping || '{}'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const syncTemplate = async (showMessage = true) => {
|
||||
if (!sceneForm.application_id) {
|
||||
ElMessage.warning('请先选择企微应用')
|
||||
return
|
||||
}
|
||||
if (!sceneForm.template_id.trim()) {
|
||||
ElMessage.warning('请输入模板 ID')
|
||||
return
|
||||
}
|
||||
|
||||
inspectingTemplate.value = true
|
||||
try {
|
||||
const response = await WecomService.inspectTemplate(sceneForm.application_id, {
|
||||
template_id: sceneForm.template_id.trim()
|
||||
})
|
||||
if (response.code === 0) {
|
||||
templateControls.value = response.data.controls || []
|
||||
templateName.value = response.data.name || ''
|
||||
syncMappingRows(templateControls.value)
|
||||
if (showMessage) {
|
||||
ElMessage.success(`模板控件已同步,共 ${templateControls.value.length} 项`)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
inspectingTemplate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [applicationResponse, sceneResponse] = await Promise.all([
|
||||
WecomService.getApplications({ page: 1, page_size: 100 }),
|
||||
WecomService.getScenes({ page: 1, page_size: 100 })
|
||||
WecomService.getScenes({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size
|
||||
})
|
||||
])
|
||||
if (applicationResponse.code === 0) applications.value = applicationResponse.data.items || []
|
||||
if (sceneResponse.code === 0) {
|
||||
scenes.value = sceneResponse.data.items || []
|
||||
const current = scenes.value.find(
|
||||
(scene) => scene.business_type === selectedBusinessType.value
|
||||
)
|
||||
fillEditor(current || scenes.value[0])
|
||||
pagination.total = sceneResponse.data.total || 0
|
||||
if (selectedBusinessType.value) {
|
||||
const current = scenes.value.find(
|
||||
(scene) => scene.business_type === selectedBusinessType.value
|
||||
)
|
||||
if (current) fillEditor(current)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
void loadData()
|
||||
}
|
||||
|
||||
const handleCurrentPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
void loadData()
|
||||
}
|
||||
|
||||
const parseMapping = (): WecomSceneControlMapping[] | undefined => {
|
||||
const result: WecomSceneControlMapping[] = []
|
||||
for (const mapping of mappingRows.value) {
|
||||
if (!mapping.business_field && !mapping.control_id && !mapping.control_type) continue
|
||||
if (!mapping.business_field || !mapping.control_id || !mapping.control_type) {
|
||||
ElMessage.warning('请完整填写控件映射字段')
|
||||
if (!mapping.business_field) {
|
||||
if (mapping.required) {
|
||||
ElMessage.warning(
|
||||
`请为必填控件“${mapping.control_title || mapping.control_id}”选择业务字段`
|
||||
)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!mapping.control_id || !mapping.control_type) {
|
||||
ElMessage.warning(`控件“${mapping.control_title || mapping.control_id}”信息不完整`)
|
||||
return
|
||||
}
|
||||
let optionMapping: Record<string, string>
|
||||
@@ -271,6 +485,7 @@
|
||||
})
|
||||
if (response.code === 0) {
|
||||
ElMessage.success('场景保存并校验成功')
|
||||
editorVisible.value = false
|
||||
await loadData()
|
||||
}
|
||||
} finally {
|
||||
@@ -278,98 +493,164 @@
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: WecomScene) => {
|
||||
if (!hasAuth(JULY_PERMISSIONS.wecom.scene)) return []
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
handler: () => editScene(row),
|
||||
type: 'primary' as const
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
onMounted(() => void loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wecom-scenes-page {
|
||||
.page-header,
|
||||
.editor-header,
|
||||
.mapping-heading,
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scene-editor-form {
|
||||
padding-bottom: 12px;
|
||||
|
||||
.dialog-section {
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.editor-header,
|
||||
.mapping-heading {
|
||||
.dialog-section + .dialog-section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.dialog-section__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
.dialog-section__title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin-top: 6px;
|
||||
.dialog-section__description {
|
||||
margin-top: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.scene-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(480px, 1fr);
|
||||
gap: 16px;
|
||||
.mapping-tip {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
.template-name {
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
.mapping-section {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.mapping-heading {
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mapping-list {
|
||||
display: flex;
|
||||
max-height: 420px;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
padding: 0 4px 4px;
|
||||
}
|
||||
|
||||
.mapping-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 1fr 0.8fr 1.3fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
.mapping-row__grid {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(120px, 1.1fr) minmax(140px, 1.3fr) minmax(120px, 1fr) minmax(100px, 0.8fr)
|
||||
minmax(140px, 1.2fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mapping-field {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mapping-field__label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.scene-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
.dialog-section {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.mapping-row {
|
||||
.dialog-section__header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mapping-tip {
|
||||
align-self: flex-start;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mapping-row__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mapping-field--option {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.mapping-row__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.mapping-field--option {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.wecom-scene-dialog) {
|
||||
width: calc(100vw - 32px) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user