+ {{ $t('feishuSync.title') }}
+ {{ $t('feishuSync.hint') }}
+ {{ $t('feishuSync.permissions') }}
+
+
+ {{ $t('feishuSync.run') }}
+ {{ $t('tls.refresh') }}
+
+ {{ $t('feishuSync.lastSuccess') }}:{{ syncStatus.last_success ? new Date(syncStatus.last_success).toLocaleString() : '—' }} · {{ $t('feishuSync.counts', { departments: syncStatus.departments || 0, members: syncStatus.members || 0 }) }}
+
+
+
+
+
+
+ {{ memberDepartments(row.department_ids) }}
+
+
+
+ {{ $t('feishuSync.firstMembers') }}
+
@@ -87,13 +109,31 @@ const profilesLoading = ref(false)
const directoryLoading = ref(false)
const directoryResult = ref('')
const directoryAvailable = ref(false)
+const syncStatus = ref({})
+const syncMembers = ref([])
+const syncDepartments = ref([])
+const syncDepartmentTree = computed(() => {
+ const nodes = new Map(syncDepartments.value.map(d => [d.id, { ...d, children: [] }]))
+ const roots = []
+ for (const node of nodes.values()) {
+ const parent = nodes.get(node.parent_id)
+ if (parent && parent !== node) parent.children.push(node)
+ else roots.push(node)
+ }
+ return roots
+})
+const syncError = ref('')
+const syncLoading = ref(false)
+const syncSaving = ref(false)
+const syncRunning = ref(false)
+let syncPollTimer
const basicProfileVerified = ref(false)
const userScopeVerified = computed(() => !!identityStatus.value?.user_scope_verified && identityForm.value.app_id === identityStatus.value?.app_id && !identityForm.value.app_secret && identityForm.value.tenant_key === identityStatus.value?.tenant_key)
const tenantSeconds = ref(0)
let tenantTimer
let identityCheckTimer
let tenantRequest = 0
-onUnmounted(() => { clearInterval(tenantTimer); clearTimeout(identityCheckTimer); tenantRequest++ })
+onUnmounted(() => { clearInterval(tenantTimer); clearTimeout(identityCheckTimer); clearTimeout(syncPollTimer); tenantRequest++ })
watch(() => [identityForm.value.app_id, identityForm.value.app_secret], ([appId, secret], [oldId, oldSecret] = []) => {
if (appId === oldId && secret === oldSecret) return
tenantRequest++
@@ -142,7 +182,7 @@ async function loadIdentity() {
try {
const { data } = await api.get('/system/identity')
identityStatus.value = data
- if (data.enabled && identitySecure) loadProfiles()
+ if (data.enabled && identitySecure) { loadProfiles(); loadSync() }
identityForm.value = { enabled: data.enabled, app_id: data.app_id || '', app_secret: '', tenant_key: data.tenant_key || '' }
tenantName.value = ''
tenantError.value = ''
@@ -170,6 +210,38 @@ async function checkDirectory() {
} catch (e) { if (subject === selectedProfile.value) { directoryAvailable.value = false; basicProfileVerified.value = false; directoryResult.value = e.response?.data?.error || t('feishuDirectory.failed') } }
finally { directoryLoading.value = false }
}
+async function loadSync() {
+ syncLoading.value = true
+ try {
+ const { data } = await api.get('/system/identity/directory-sync')
+ syncStatus.value = data.status || {}
+ syncMembers.value = data.members || []
+ syncDepartments.value = data.departments || []
+ syncError.value = ''
+ syncRunning.value = !!data.running
+ clearTimeout(syncPollTimer)
+ if (data.running) syncPollTimer = setTimeout(loadSync, 3000)
+ } catch (e) { syncError.value = e.response?.data?.error || t('feishuSync.failed') }
+ finally { syncLoading.value = false }
+}
+async function setSyncEnabled(enabled) {
+ syncSaving.value = true
+ try {
+ const { data } = await api.put('/system/identity/directory-sync', { enabled })
+ syncStatus.value = data.status
+ ElMessage.success(t('feishuSync.saved'))
+ } catch (e) { ElMessage.error(e.response?.data?.error || t('feishuSync.failed')) }
+ finally { syncSaving.value = false }
+}
+async function runSync() {
+ syncRunning.value = true
+ syncError.value = ''
+ try { await api.post('/system/identity/directory-sync/run'); clearTimeout(syncPollTimer); syncPollTimer = setTimeout(loadSync, 1500); ElMessage.success(t('feishuSync.started')) }
+ catch (e) { syncRunning.value = false; syncError.value = e.response?.data?.error || t('feishuSync.failed') }
+}
+function memberDepartments(value) {
+ try { return JSON.parse(value || '[]').join(', ') || '—' } catch { return '—' }
+}
async function saveIdentity() {
if (!identitySecure) return ElMessage.warning(t('identity.httpsOnly'))
identitySaving.value = true