|
|
@@ -23,7 +23,8 @@
|
|
|
<el-option label="正常" value="ACTIVE" />
|
|
|
<el-option label="已禁用" value="DISABLED" />
|
|
|
</el-select>
|
|
|
- <el-select v-model="roleFilter" placeholder="角色筛选" clearable @change="handleSearch" style="width: 120px">
|
|
|
+ <el-select v-model="roleFilter" placeholder="角色筛选" clearable @change="handleSearch" style="width: 140px">
|
|
|
+ <el-option label="普通用户" value="ORDINARY_USER" />
|
|
|
<el-option label="开发者" value="DEVELOPER" />
|
|
|
<el-option label="管理员" value="SUPER_ADMIN" />
|
|
|
</el-select>
|
|
|
@@ -125,6 +126,13 @@
|
|
|
>
|
|
|
重置密码
|
|
|
</el-dropdown-item>
|
|
|
+ <el-dropdown-item
|
|
|
+ v-if="isSuperAdmin && scope.row.role === 'ORDINARY_USER'"
|
|
|
+ class="danger-dropdown-item"
|
|
|
+ @click="openDeleteUserDialog(scope.row)"
|
|
|
+ >
|
|
|
+ 删除用户
|
|
|
+ </el-dropdown-item>
|
|
|
</el-dropdown-menu>
|
|
|
</template>
|
|
|
</el-dropdown>
|
|
|
@@ -176,6 +184,46 @@
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
+ <!-- Delete user (ordinary only): password + SMS -->
|
|
|
+ <el-dialog v-model="deleteUserDialogVisible" title="删除用户" width="440px" @closed="resetDeleteUserForm">
|
|
|
+ <p class="delete-hint">
|
|
|
+ 将软删除用户 <strong>{{ deleteTarget?.mobile }}</strong>(不可登录)。需验证您的登录密码与本人手机号短信验证码。
|
|
|
+ </p>
|
|
|
+ <p v-if="maskedAdminMobile" class="text-muted">验证码发送至:<strong>{{ maskedAdminMobile }}</strong></p>
|
|
|
+ <el-form label-position="top" style="margin-top: 12px;">
|
|
|
+ <el-form-item label="登录密码" required>
|
|
|
+ <el-input
|
|
|
+ v-model="deleteUserForm.password"
|
|
|
+ type="password"
|
|
|
+ show-password
|
|
|
+ placeholder="请输入您的登录密码"
|
|
|
+ autocomplete="new-password"
|
|
|
+ :name="'del_pwd_' + dynamicPwdField"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="短信验证码" required>
|
|
|
+ <div class="sms-row">
|
|
|
+ <el-input
|
|
|
+ v-model="deleteUserForm.sms_code"
|
|
|
+ placeholder="6 位验证码"
|
|
|
+ maxlength="6"
|
|
|
+ @keyup.enter="confirmDeleteUser"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ :disabled="deleteSmsCountdown > 0 || !adminMobile"
|
|
|
+ @click="sendDeleteUserSms"
|
|
|
+ >
|
|
|
+ {{ deleteSmsCountdown > 0 ? `${deleteSmsCountdown}s` : '获取验证码' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="deleteUserDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="danger" :loading="deletingUser" @click="confirmDeleteUser">确认删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
<!-- Admin Verify Dialog for Status/Reset -->
|
|
|
<el-dialog v-model="verifyDialogVisible" title="安全验证" width="400px">
|
|
|
<p>此操作需要验证管理员权限。</p>
|
|
|
@@ -404,13 +452,25 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted, reactive } from 'vue'
|
|
|
+import { ref, onMounted, onUnmounted, reactive, computed } from 'vue'
|
|
|
import { ElMessage, FormInstance, FormRules } from 'element-plus'
|
|
|
import { Refresh, ArrowDown, Search, Plus, List, Upload, EditPen, Warning } from '@element-plus/icons-vue'
|
|
|
import api from '../utils/request'
|
|
|
import { getLogs, OperationLog } from '../api/logs'
|
|
|
+import { sendSmsCode } from '../api/smsAuth'
|
|
|
+import { deleteUserWithVerification } from '../api/users'
|
|
|
+import { useAuthStore } from '../store/auth'
|
|
|
import UserImportDialog from '../components/UserImportDialog.vue'
|
|
|
|
|
|
+const authStore = useAuthStore()
|
|
|
+const isSuperAdmin = computed(() => authStore.user?.role === 'SUPER_ADMIN')
|
|
|
+const adminMobile = computed(() => (authStore.user?.mobile as string) || '')
|
|
|
+const maskedAdminMobile = computed(() => {
|
|
|
+ const m = adminMobile.value
|
|
|
+ if (!m || m.length < 11) return ''
|
|
|
+ return `${m.slice(0, 3)}****${m.slice(-4)}`
|
|
|
+})
|
|
|
+
|
|
|
interface User {
|
|
|
id: number
|
|
|
mobile: string
|
|
|
@@ -784,6 +844,91 @@ const handleBatchResetClick = () => {
|
|
|
batchResetDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
+const deleteUserDialogVisible = ref(false)
|
|
|
+const deleteTarget = ref<User | null>(null)
|
|
|
+const deletingUser = ref(false)
|
|
|
+const deleteUserForm = reactive({
|
|
|
+ password: '',
|
|
|
+ sms_code: ''
|
|
|
+})
|
|
|
+const deleteSmsCountdown = ref(0)
|
|
|
+let deleteSmsTimer: ReturnType<typeof setInterval> | null = null
|
|
|
+
|
|
|
+const resetDeleteUserForm = () => {
|
|
|
+ deleteTarget.value = null
|
|
|
+ deleteUserForm.password = ''
|
|
|
+ deleteUserForm.sms_code = ''
|
|
|
+}
|
|
|
+
|
|
|
+const openDeleteUserDialog = async (user: User) => {
|
|
|
+ if (user.role !== 'ORDINARY_USER') return
|
|
|
+ if (!authStore.user?.mobile) {
|
|
|
+ try {
|
|
|
+ await authStore.fetchUser()
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('无法获取当前账号信息,请重新登录')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!adminMobile.value) {
|
|
|
+ ElMessage.error('当前账号无手机号,无法发送验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ deleteTarget.value = user
|
|
|
+ deleteUserForm.password = ''
|
|
|
+ deleteUserForm.sms_code = ''
|
|
|
+ refreshDynamicField()
|
|
|
+ deleteUserDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const sendDeleteUserSms = async () => {
|
|
|
+ if (!adminMobile.value) {
|
|
|
+ ElMessage.warning('无法获取手机号')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await sendSmsCode(adminMobile.value, 'pc')
|
|
|
+ ElMessage.success('验证码已发送')
|
|
|
+ deleteSmsCountdown.value = 60
|
|
|
+ if (deleteSmsTimer) clearInterval(deleteSmsTimer)
|
|
|
+ deleteSmsTimer = setInterval(() => {
|
|
|
+ deleteSmsCountdown.value--
|
|
|
+ if (deleteSmsCountdown.value <= 0 && deleteSmsTimer) {
|
|
|
+ clearInterval(deleteSmsTimer)
|
|
|
+ deleteSmsTimer = null
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ } catch {
|
|
|
+ // interceptor
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const confirmDeleteUser = async () => {
|
|
|
+ if (!deleteTarget.value) return
|
|
|
+ if (!deleteUserForm.password) {
|
|
|
+ ElMessage.warning('请输入登录密码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!deleteUserForm.sms_code) {
|
|
|
+ ElMessage.warning('请输入短信验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ deletingUser.value = true
|
|
|
+ try {
|
|
|
+ await deleteUserWithVerification(deleteTarget.value.id, {
|
|
|
+ password: deleteUserForm.password,
|
|
|
+ sms_code: deleteUserForm.sms_code
|
|
|
+ })
|
|
|
+ ElMessage.success('用户已删除')
|
|
|
+ deleteUserDialogVisible.value = false
|
|
|
+ fetchUsers()
|
|
|
+ } catch {
|
|
|
+ // interceptor
|
|
|
+ } finally {
|
|
|
+ deletingUser.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const confirmBatchReset = async () => {
|
|
|
if (!batchAdminPassword.value) {
|
|
|
ElMessage.warning('请输入管理员密码')
|
|
|
@@ -879,6 +1024,16 @@ const getActionLabel = (type: string) => {
|
|
|
|
|
|
onMounted(() => {
|
|
|
fetchUsers()
|
|
|
+ if (authStore.token && !authStore.user) {
|
|
|
+ authStore.fetchUser().catch(() => {})
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (deleteSmsTimer) {
|
|
|
+ clearInterval(deleteSmsTimer)
|
|
|
+ deleteSmsTimer = null
|
|
|
+ }
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
@@ -936,4 +1091,25 @@ onMounted(() => {
|
|
|
.captcha-item {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
+.delete-hint {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.text-muted {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+.sms-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.sms-row .el-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+:deep(.danger-dropdown-item) {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
</style>
|