|
|
@@ -28,6 +28,16 @@
|
|
|
<el-option label="开发者" value="DEVELOPER" />
|
|
|
<el-option label="管理员" value="SUPER_ADMIN" />
|
|
|
</el-select>
|
|
|
+ <el-select
|
|
|
+ v-model="organizationFilter"
|
|
|
+ placeholder="组织筛选"
|
|
|
+ clearable
|
|
|
+ @change="handleSearch"
|
|
|
+ style="width: 180px"
|
|
|
+ >
|
|
|
+ <el-option label="未分配" :value="0" />
|
|
|
+ <el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
|
|
+ </el-select>
|
|
|
|
|
|
<el-divider direction="vertical" class="action-divider" />
|
|
|
|
|
|
@@ -43,6 +53,16 @@
|
|
|
<el-button type="warning" plain @click="handleBatchResetClick" :disabled="selectedUsers.length === 0">
|
|
|
<el-icon style="margin-right: 4px"><EditPen /></el-icon> 批量重置英文名
|
|
|
</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="isSuperAdmin"
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ @click="openBatchOrgDialog"
|
|
|
+ :disabled="selectedUsers.length === 0"
|
|
|
+ >
|
|
|
+ 批量设置组织
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isSuperAdmin" plain @click="openOrgManageDialog">组织管理</el-button>
|
|
|
<el-button @click="openLogDrawer">
|
|
|
<el-icon style="margin-right: 4px"><List /></el-icon> 操作日志
|
|
|
</el-button>
|
|
|
@@ -62,6 +82,11 @@
|
|
|
<el-table-column prop="mobile" label="手机号" min-width="120" />
|
|
|
<el-table-column prop="name" label="姓名" min-width="100" />
|
|
|
<el-table-column prop="english_name" label="英文名" min-width="120" />
|
|
|
+ <el-table-column prop="organization_name" label="所属组织" min-width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ scope.row.organization_name || '—' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<el-table-column prop="role" label="角色" width="120" align="center">
|
|
|
<template #default="scope">
|
|
|
<el-tag :type="scope.row.role === 'SUPER_ADMIN' ? 'danger' : (scope.row.role === 'DEVELOPER' ? 'warning' : 'info')">
|
|
|
@@ -269,6 +294,82 @@
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
+ <!-- Batch set organization -->
|
|
|
+ <el-dialog v-model="batchOrgDialogVisible" title="批量设置组织" width="440px">
|
|
|
+ <p style="margin-bottom: 12px; color: #606266">
|
|
|
+ 已选择 <strong>{{ selectedUsers.length }}</strong> 位用户;可选择组织或取消归属。
|
|
|
+ </p>
|
|
|
+ <el-form label-position="top">
|
|
|
+ <el-form-item label="所属组织">
|
|
|
+ <el-select v-model="batchOrgTargetId" clearable placeholder="不选择则表示取消归属" style="width: 100%">
|
|
|
+ <el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="管理员密码验证" required>
|
|
|
+ <el-input
|
|
|
+ v-model="batchOrgPassword"
|
|
|
+ type="password"
|
|
|
+ show-password
|
|
|
+ placeholder="请输入管理员密码"
|
|
|
+ autocomplete="new-password"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="batchOrgDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="batchOrgLoading" @click="confirmBatchOrg">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- Organization CRUD (super admin) -->
|
|
|
+ <el-dialog v-model="orgManageVisible" title="组织管理" width="640px" @open="loadOrgListForManage">
|
|
|
+ <p class="text-muted" style="margin: 0 0 12px">组织创建后不可删除,仅可编辑;编辑时需验证图形验证码。</p>
|
|
|
+ <div style="margin-bottom: 16px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center">
|
|
|
+ <el-input v-model="newOrgName" placeholder="新组织名称" style="width: 200px" clearable />
|
|
|
+ <el-input v-model="newOrgDesc" placeholder="描述(可选)" style="width: 200px" clearable />
|
|
|
+ <el-input-number v-model="newOrgSort" :min="0" placeholder="排序" style="width: 120px" />
|
|
|
+ <el-button type="primary" :loading="orgCreating" @click="submitNewOrg">添加</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="orgList" v-loading="orgManageLoading" border stripe size="small" max-height="360">
|
|
|
+ <el-table-column prop="id" label="ID" width="70" />
|
|
|
+ <el-table-column prop="name" label="名称" min-width="120" />
|
|
|
+ <el-table-column prop="description" label="描述" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="sort_order" label="排序" width="80" />
|
|
|
+ <el-table-column label="操作" width="80" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button type="primary" link @click="openEditOrg(scope.row)">编辑</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <el-dialog v-model="editOrgVisible" title="编辑组织" width="480px" @opened="refreshEditOrgCaptcha">
|
|
|
+ <p class="text-muted" style="margin-top: 0">请填写图形验证码以确认管理员身份。</p>
|
|
|
+ <el-form label-width="88px">
|
|
|
+ <el-form-item label="名称" required>
|
|
|
+ <el-input v-model="editOrgForm.name" maxlength="100" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="描述">
|
|
|
+ <el-input v-model="editOrgForm.description" type="textarea" rows="2" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="排序">
|
|
|
+ <el-input-number v-model="editOrgForm.sort_order" :min="0" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="图形验证码" required class="captcha-item">
|
|
|
+ <div class="captcha-row-edit">
|
|
|
+ <el-input v-model="editOrgForm.captcha_code" placeholder="请输入右侧验证码" maxlength="10" />
|
|
|
+ <div class="captcha-img" @click="refreshEditOrgCaptcha" v-if="editOrgCaptchaImage">
|
|
|
+ <img :src="editOrgCaptchaImage" alt="captcha" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="editOrgVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="editOrgSubmitting" @click="submitEditOrg">保存</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
<!-- Batch Reset Dialog -->
|
|
|
<el-dialog v-model="batchResetDialogVisible" title="批量重置英文名" width="400px">
|
|
|
<div class="warning-text" style="margin-bottom: 20px; color: #e6a23c; display: flex; align-items: flex-start; gap: 8px;">
|
|
|
@@ -321,6 +422,11 @@
|
|
|
<el-option label="超级管理员" value="SUPER_ADMIN" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item v-if="isSuperAdmin" label="所属组织">
|
|
|
+ <el-select v-model="createForm.organization_id" clearable placeholder="未分配" style="width: 100%">
|
|
|
+ <el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="管理员验证" prop="admin_password">
|
|
|
<el-input
|
|
|
v-model="createForm.admin_password"
|
|
|
@@ -350,6 +456,11 @@
|
|
|
<el-form-item label="英文名" prop="english_name">
|
|
|
<el-input v-model="editForm.english_name" placeholder="请输入英文名(选填,自动生成拼音)" />
|
|
|
</el-form-item>
|
|
|
+ <el-form-item v-if="isSuperAdmin" label="所属组织">
|
|
|
+ <el-select v-model="editForm.organization_id" clearable placeholder="未分配" style="width: 100%">
|
|
|
+ <el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="管理员验证" prop="admin_password">
|
|
|
<el-input
|
|
|
v-model="editForm.admin_password"
|
|
|
@@ -383,6 +494,8 @@
|
|
|
<el-option label="启用" value="ENABLE" />
|
|
|
<el-option label="变更角色" value="CHANGE_ROLE" />
|
|
|
<el-option label="重置密码" value="RESET_PASSWORD" />
|
|
|
+ <el-option label="新建组织" value="ORG_CREATE" />
|
|
|
+ <el-option label="编辑组织" value="ORG_UPDATE" />
|
|
|
</el-select>
|
|
|
<el-input
|
|
|
v-model="logFilter.keyword"
|
|
|
@@ -462,7 +575,14 @@ import { Refresh, ArrowDown, Search, Plus, List, Upload, EditPen, Warning } from
|
|
|
import api from '../utils/request'
|
|
|
import { getLogs, OperationLog } from '../api/logs'
|
|
|
import { sendSmsCode } from '../api/smsAuth'
|
|
|
-import { deleteUserWithVerification } from '../api/users'
|
|
|
+import { deleteUserWithVerification, batchSetOrganization } from '../api/users'
|
|
|
+import {
|
|
|
+ getOrganizations,
|
|
|
+ createOrganization,
|
|
|
+ updateOrganization,
|
|
|
+ type Organization,
|
|
|
+} from '../api/organizations'
|
|
|
+import { getCaptcha } from '../api/public'
|
|
|
import { useAuthStore } from '../store/auth'
|
|
|
import UserImportDialog from '../components/UserImportDialog.vue'
|
|
|
|
|
|
@@ -483,14 +603,28 @@ interface User {
|
|
|
status: string
|
|
|
role: string
|
|
|
created_at: string
|
|
|
+ organization_id?: number | null
|
|
|
+ organization_name?: string | null
|
|
|
}
|
|
|
|
|
|
const users = ref<User[]>([])
|
|
|
const loading = ref(false)
|
|
|
const statusFilter = ref('')
|
|
|
const roleFilter = ref('')
|
|
|
+const organizationFilter = ref<number | undefined>(undefined)
|
|
|
const searchQuery = ref('')
|
|
|
|
|
|
+const orgList = ref<Organization[]>([])
|
|
|
+
|
|
|
+const fetchOrganizations = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getOrganizations()
|
|
|
+ orgList.value = res.data || []
|
|
|
+ } catch {
|
|
|
+ orgList.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const currentPage = ref(1)
|
|
|
const pageSize = ref(10)
|
|
|
const total = ref(0)
|
|
|
@@ -507,7 +641,8 @@ const createForm = reactive({
|
|
|
english_name: '',
|
|
|
password: '',
|
|
|
role: 'ORDINARY_USER',
|
|
|
- admin_password: ''
|
|
|
+ admin_password: '',
|
|
|
+ organization_id: null as number | null,
|
|
|
})
|
|
|
|
|
|
// Edit User Logic
|
|
|
@@ -519,7 +654,8 @@ const editForm = reactive({
|
|
|
mobile: '',
|
|
|
name: '',
|
|
|
english_name: '',
|
|
|
- admin_password: ''
|
|
|
+ admin_password: '',
|
|
|
+ organization_id: null as number | null,
|
|
|
})
|
|
|
const editRules = reactive<FormRules>({
|
|
|
mobile: [
|
|
|
@@ -539,6 +675,7 @@ const handleEditUser = (user: User) => {
|
|
|
editForm.mobile = user.mobile
|
|
|
editForm.name = user.name || ''
|
|
|
editForm.english_name = user.english_name || ''
|
|
|
+ editForm.organization_id = user.organization_id ?? null
|
|
|
editForm.admin_password = ''
|
|
|
refreshDynamicField()
|
|
|
editDialogVisible.value = true
|
|
|
@@ -554,6 +691,7 @@ const submitEditUser = async () => {
|
|
|
mobile: editForm.mobile,
|
|
|
name: editForm.name,
|
|
|
english_name: editForm.english_name,
|
|
|
+ organization_id: editForm.organization_id,
|
|
|
admin_password: editForm.admin_password
|
|
|
})
|
|
|
ElMessage.success('用户信息更新成功')
|
|
|
@@ -601,6 +739,7 @@ const handleCreateUser = () => {
|
|
|
createForm.password = ''
|
|
|
createForm.role = 'ORDINARY_USER'
|
|
|
createForm.admin_password = ''
|
|
|
+ createForm.organization_id = null
|
|
|
refreshDynamicField()
|
|
|
createDialogVisible.value = true
|
|
|
}
|
|
|
@@ -634,6 +773,9 @@ const fetchUsers = async () => {
|
|
|
if (statusFilter.value) params.status = statusFilter.value
|
|
|
if (roleFilter.value) params.role = roleFilter.value
|
|
|
if (searchQuery.value) params.keyword = searchQuery.value
|
|
|
+ if (organizationFilter.value !== undefined) {
|
|
|
+ params.organization_id = organizationFilter.value
|
|
|
+ }
|
|
|
|
|
|
const res = await api.get('/users/', { params })
|
|
|
if (res.data && Array.isArray(res.data.items)) {
|
|
|
@@ -1013,13 +1155,161 @@ const getActionLabel = (type: string) => {
|
|
|
'ENABLE': '启用',
|
|
|
'RESET_PASSWORD': '重置密码',
|
|
|
'CHANGE_ROLE': '变更角色',
|
|
|
- 'SYNC_M2M': 'M2M 同步'
|
|
|
+ 'SYNC_M2M': 'M2M 同步',
|
|
|
+ 'ORG_CREATE': '新建组织',
|
|
|
+ 'ORG_UPDATE': '编辑组织',
|
|
|
}
|
|
|
return map[type] || type
|
|
|
}
|
|
|
|
|
|
+const batchOrgDialogVisible = ref(false)
|
|
|
+const batchOrgTargetId = ref<number | undefined>(undefined)
|
|
|
+const batchOrgPassword = ref('')
|
|
|
+const batchOrgLoading = ref(false)
|
|
|
+
|
|
|
+const orgManageVisible = ref(false)
|
|
|
+const orgManageLoading = ref(false)
|
|
|
+const newOrgName = ref('')
|
|
|
+const newOrgDesc = ref('')
|
|
|
+const newOrgSort = ref(0)
|
|
|
+const orgCreating = ref(false)
|
|
|
+
|
|
|
+const editOrgVisible = ref(false)
|
|
|
+const editOrgSubmitting = ref(false)
|
|
|
+const editOrgCaptchaImage = ref('')
|
|
|
+const editOrgForm = reactive({
|
|
|
+ id: 0,
|
|
|
+ name: '',
|
|
|
+ description: '',
|
|
|
+ sort_order: 0,
|
|
|
+ captcha_id: '',
|
|
|
+ captcha_code: '',
|
|
|
+})
|
|
|
+
|
|
|
+const refreshEditOrgCaptcha = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getCaptcha()
|
|
|
+ editOrgCaptchaImage.value = res.data.image
|
|
|
+ editOrgForm.captcha_id = res.data.captcha_id
|
|
|
+ editOrgForm.captcha_code = ''
|
|
|
+ } catch {
|
|
|
+ editOrgCaptchaImage.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openEditOrg = (row: Organization) => {
|
|
|
+ editOrgForm.id = row.id
|
|
|
+ editOrgForm.name = row.name
|
|
|
+ editOrgForm.description = row.description ?? ''
|
|
|
+ editOrgForm.sort_order = row.sort_order
|
|
|
+ editOrgForm.captcha_code = ''
|
|
|
+ editOrgVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const submitEditOrg = async () => {
|
|
|
+ const name = editOrgForm.name.trim()
|
|
|
+ if (!name) {
|
|
|
+ ElMessage.warning('请输入组织名称')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!editOrgForm.captcha_code) {
|
|
|
+ ElMessage.warning('请输入图形验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ editOrgSubmitting.value = true
|
|
|
+ try {
|
|
|
+ const desc = editOrgForm.description.trim()
|
|
|
+ await updateOrganization(editOrgForm.id, {
|
|
|
+ name,
|
|
|
+ description: desc || undefined,
|
|
|
+ sort_order: editOrgForm.sort_order,
|
|
|
+ captcha_id: editOrgForm.captcha_id,
|
|
|
+ captcha_code: editOrgForm.captcha_code,
|
|
|
+ })
|
|
|
+ ElMessage.success('已保存')
|
|
|
+ editOrgVisible.value = false
|
|
|
+ await fetchOrganizations()
|
|
|
+ } catch {
|
|
|
+ refreshEditOrgCaptcha()
|
|
|
+ } finally {
|
|
|
+ editOrgSubmitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadOrgListForManage = async () => {
|
|
|
+ orgManageLoading.value = true
|
|
|
+ try {
|
|
|
+ await fetchOrganizations()
|
|
|
+ } finally {
|
|
|
+ orgManageLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openOrgManageDialog = () => {
|
|
|
+ orgManageVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const submitNewOrg = async () => {
|
|
|
+ const name = newOrgName.value.trim()
|
|
|
+ if (!name) {
|
|
|
+ ElMessage.warning('请输入组织名称')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ orgCreating.value = true
|
|
|
+ try {
|
|
|
+ await createOrganization({
|
|
|
+ name,
|
|
|
+ description: newOrgDesc.value.trim() || undefined,
|
|
|
+ sort_order: newOrgSort.value ?? 0,
|
|
|
+ })
|
|
|
+ ElMessage.success('已添加')
|
|
|
+ newOrgName.value = ''
|
|
|
+ newOrgDesc.value = ''
|
|
|
+ newOrgSort.value = 0
|
|
|
+ await fetchOrganizations()
|
|
|
+ } catch {
|
|
|
+ //
|
|
|
+ } finally {
|
|
|
+ orgCreating.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openBatchOrgDialog = () => {
|
|
|
+ if (selectedUsers.value.length === 0) {
|
|
|
+ ElMessage.warning('请先选择用户')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ batchOrgTargetId.value = undefined
|
|
|
+ batchOrgPassword.value = ''
|
|
|
+ batchOrgDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const confirmBatchOrg = async () => {
|
|
|
+ if (!batchOrgPassword.value) {
|
|
|
+ ElMessage.warning('请输入管理员密码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ batchOrgLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await batchSetOrganization({
|
|
|
+ user_ids: selectedUsers.value.map((u) => u.id),
|
|
|
+ organization_id: batchOrgTargetId.value ?? null,
|
|
|
+ admin_password: batchOrgPassword.value,
|
|
|
+ })
|
|
|
+ ElMessage.success(`已更新 ${res.data.count} 位用户的组织`)
|
|
|
+ batchOrgDialogVisible.value = false
|
|
|
+ fetchUsers()
|
|
|
+ selectedUsers.value = []
|
|
|
+ } catch {
|
|
|
+ //
|
|
|
+ } finally {
|
|
|
+ batchOrgLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
fetchUsers()
|
|
|
+ fetchOrganizations()
|
|
|
if (authStore.token && !authStore.user) {
|
|
|
authStore.fetchUser().catch(() => {})
|
|
|
}
|
|
|
@@ -1105,6 +1395,26 @@ onUnmounted(() => {
|
|
|
.sms-row .el-input {
|
|
|
flex: 1;
|
|
|
}
|
|
|
+.captcha-row-edit {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.captcha-row-edit .el-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.captcha-img {
|
|
|
+ cursor: pointer;
|
|
|
+ flex-shrink: 0;
|
|
|
+ height: 40px;
|
|
|
+ line-height: 0;
|
|
|
+}
|
|
|
+.captcha-img img {
|
|
|
+ height: 40px;
|
|
|
+ display: block;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
:deep(.danger-dropdown-item) {
|
|
|
color: #f56c6c;
|
|
|
}
|