|
@@ -99,27 +99,29 @@
|
|
|
<template #last_login_type="{ record }">
|
|
<template #last_login_type="{ record }">
|
|
|
<span>{{ formatLastLoginType(record.last_login_type) }}</span>
|
|
<span>{{ formatLastLoginType(record.last_login_type) }}</span>
|
|
|
</template>
|
|
</template>
|
|
|
- <template #send_count_auto_approve="{ record }">
|
|
|
|
|
- <a-switch
|
|
|
|
|
- :model-value="isSendCountAutoApprove(record)"
|
|
|
|
|
- :loading="whitelistLoadingUuid === record.uuid"
|
|
|
|
|
- @change="(checked) => onSendCountAutoApproveChange(record, checked)"
|
|
|
|
|
- />
|
|
|
|
|
- </template>
|
|
|
|
|
<template #is_banned_status="{ record }">
|
|
<template #is_banned_status="{ record }">
|
|
|
<a-tag v-if="isBanned(record)" color="red">已封禁</a-tag>
|
|
<a-tag v-if="isBanned(record)" color="red">已封禁</a-tag>
|
|
|
<a-tag v-else color="green">正常</a-tag>
|
|
<a-tag v-else color="green">正常</a-tag>
|
|
|
</template>
|
|
</template>
|
|
|
- <template #is_banned="{ record }">
|
|
|
|
|
- <a-switch
|
|
|
|
|
- :model-value="isBanned(record)"
|
|
|
|
|
- :loading="banLoadingUuid === record.uuid"
|
|
|
|
|
- checked-color="rgb(var(--red-6))"
|
|
|
|
|
- @change="(checked) => onBanChange(record, checked)"
|
|
|
|
|
- />
|
|
|
|
|
- </template>
|
|
|
|
|
<template #optional="{ record }">
|
|
<template #optional="{ record }">
|
|
|
- <a-button @click="changeCount(record)">更改次数</a-button>
|
|
|
|
|
|
|
+ <a-dropdown :popup-max-height="false" trigger="hover">
|
|
|
|
|
+ <a-button>操作 <icon-down /></a-button>
|
|
|
|
|
+ <template #content>
|
|
|
|
|
+ <a-doption v-if="hasPermission('action.user.changeCount')" @click="changeCount(record)">
|
|
|
|
|
+ <icon-edit /> 更改次数
|
|
|
|
|
+ </a-doption>
|
|
|
|
|
+ <a-doption v-if="hasPermission('action.user.setSendCountAutoApprove')" @click="onSendCountAutoApproveChange(record, !isSendCountAutoApprove(record))">
|
|
|
|
|
+ <icon-check-circle /> {{ isSendCountAutoApprove(record) ? '关闭赠送免审' : '开启赠送免审' }}
|
|
|
|
|
+ </a-doption>
|
|
|
|
|
+ <a-doption v-if="hasPermission('action.user.ban')" @click="onBanChange(record, !isBanned(record))">
|
|
|
|
|
+ <icon-minus-circle /> {{ isBanned(record) ? '解除封禁' : '封禁账户' }}
|
|
|
|
|
+ </a-doption>
|
|
|
|
|
+ <a-doption v-if="hasPermission('action.user.permissionManage')" @click="openPermissionManage(record)">
|
|
|
|
|
+ <icon-user /> 权限管理
|
|
|
|
|
+ </a-doption>
|
|
|
|
|
+ <a-doption v-if="!hasAnyUserOperationPermission" disabled>暂无可用操作</a-doption>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-dropdown>
|
|
|
</template>
|
|
</template>
|
|
|
</a-table>
|
|
</a-table>
|
|
|
</a-card>
|
|
</a-card>
|
|
@@ -140,12 +142,91 @@
|
|
|
</a-form-item>
|
|
</a-form-item>
|
|
|
</a-form>
|
|
</a-form>
|
|
|
</a-modal>
|
|
</a-modal>
|
|
|
|
|
+
|
|
|
|
|
+ <a-modal v-model:visible="permissionVisible" title="权限管理" width="860px" @before-ok="handlePermissionBeforeOk"
|
|
|
|
|
+ :ok-loading="permissionSaving" :mask-closable="false" draggable esc-to-close closable>
|
|
|
|
|
+ <a-spin :loading="permissionLoading">
|
|
|
|
|
+ <a-tabs v-model:active-key="permissionTab">
|
|
|
|
|
+ <a-tab-pane key="user" title="用户权限">
|
|
|
|
|
+ <a-space direction="vertical" fill>
|
|
|
|
|
+ <a-alert>
|
|
|
|
|
+ 正在配置用户 {{ permissionUser?.username || permissionUser?.uuid || '-' }} 的权限。乐跑账号、赠送次数、发起工单等基础操作默认全员开放,取消勾选即可单独关闭;管理端等高级权限需额外勾选。旧角色继承权限会展示在下方,但不会随本窗口保存而改变。
|
|
|
|
|
+ </a-alert>
|
|
|
|
|
+ <a-radio-group v-model="permissionCategory" type="button">
|
|
|
|
|
+ <a-radio value="">全部</a-radio>
|
|
|
|
|
+ <a-radio value="page">页面</a-radio>
|
|
|
|
|
+ <a-radio value="action">操作</a-radio>
|
|
|
|
|
+ </a-radio-group>
|
|
|
|
|
+ <a-checkbox-group v-model="selectedPermissionCodes" class="permission-list">
|
|
|
|
|
+ <a-grid :cols="2" :col-gap="12" :row-gap="12">
|
|
|
|
|
+ <a-grid-item v-for="item in filteredPermissionPoints" :key="item.code">
|
|
|
|
|
+ <a-card size="small" :bordered="true" :class="{ 'permission-card-basic': isBasicPermission(item.code) }">
|
|
|
|
|
+ <a-checkbox :value="item.code">
|
|
|
|
|
+ <div class="permission-title">
|
|
|
|
|
+ {{ item.name }}
|
|
|
|
|
+ <a-tag v-if="isBasicPermission(item.code)" size="small" color="arcoblue">默认开放</a-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="permission-code">{{ item.code }}</div>
|
|
|
|
|
+ <div v-if="item.remark" class="permission-remark">{{ item.remark }}</div>
|
|
|
|
|
+ </a-checkbox>
|
|
|
|
|
+ </a-card>
|
|
|
|
|
+ </a-grid-item>
|
|
|
|
|
+ </a-grid>
|
|
|
|
|
+ </a-checkbox-group>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="permission-section-title">当前生效权限</div>
|
|
|
|
|
+ <a-space wrap>
|
|
|
|
|
+ <a-tag v-for="code in effectivePermissionCodes" :key="code">{{ code }}</a-tag>
|
|
|
|
|
+ <span v-if="effectivePermissionCodes.length === 0" class="muted">暂无</span>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ </a-tab-pane>
|
|
|
|
|
+ <a-tab-pane key="resource" title="权限规则">
|
|
|
|
|
+ <a-alert style="margin-bottom: 12px;">
|
|
|
|
|
+ 这里配置页面、操作或接口需要哪些权限。关闭规则或清空权限后,该资源默认不需要权限。
|
|
|
|
|
+ </a-alert>
|
|
|
|
|
+ <div class="permission-resource-list">
|
|
|
|
|
+ <a-card v-for="rule in permissionResources" :key="rule.id" size="small" class="permission-resource-card">
|
|
|
|
|
+ <a-space direction="vertical" fill>
|
|
|
|
|
+ <a-row align="center">
|
|
|
|
|
+ <a-col :flex="1">
|
|
|
|
|
+ <div class="permission-title">{{ formatResourceRule(rule) }}</div>
|
|
|
|
|
+ <div class="permission-code">{{ rule.resource_type }}:{{ rule.resource_key }}</div>
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ <a-col>
|
|
|
|
|
+ <a-switch v-model="rule.enabled" :checked-value="1" :unchecked-value="0" />
|
|
|
|
|
+ </a-col>
|
|
|
|
|
+ </a-row>
|
|
|
|
|
+ <a-select v-model="rule.required_codes" multiple allow-search allow-clear placeholder="请选择该资源需要的权限点">
|
|
|
|
|
+ <a-option v-for="item in permissionPoints" :key="item.code" :value="item.code">
|
|
|
|
|
+ {{ item.name }}({{ item.code }})
|
|
|
|
|
+ </a-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ <a-button size="small" type="primary" :loading="resourceSavingId === rule.id" @click="saveResourceRule(rule)">
|
|
|
|
|
+ 保存规则
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ </a-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </a-tab-pane>
|
|
|
|
|
+ </a-tabs>
|
|
|
|
|
+ </a-spin>
|
|
|
|
|
+ </a-modal>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
import { adminGetUserList, adminChangeLepaoCount, adminSetSendCountAutoApprove, adminSetUserBan } from '@/api/user'
|
|
import { adminGetUserList, adminChangeLepaoCount, adminSetSendCountAutoApprove, adminSetUserBan } from '@/api/user'
|
|
|
|
|
+import { getPermissionPoints, getPermissionResources, getUserPermissions, setUserPermissions, updatePermissionResource } from '@/api/permission'
|
|
|
import { Notification, Message, Modal } from '@arco-design/web-vue'
|
|
import { Notification, Message, Modal } from '@arco-design/web-vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ buildSelectedPermissionCodes,
|
|
|
|
|
+ extractDeniedBasicPermissionCodes,
|
|
|
|
|
+ filterSavablePermissionCodes,
|
|
|
|
|
+ hasPermission,
|
|
|
|
|
+ isBasicPermission
|
|
|
|
|
+} from '@/utils/permission'
|
|
|
|
|
|
|
|
const visible = ref(false)
|
|
const visible = ref(false)
|
|
|
const ok_loading = ref(false)
|
|
const ok_loading = ref(false)
|
|
@@ -172,6 +253,31 @@ const loading = ref(false)
|
|
|
const data = ref([])
|
|
const data = ref([])
|
|
|
const whitelistLoadingUuid = ref('')
|
|
const whitelistLoadingUuid = ref('')
|
|
|
const banLoadingUuid = ref('')
|
|
const banLoadingUuid = ref('')
|
|
|
|
|
+const permissionVisible = ref(false)
|
|
|
|
|
+const permissionLoading = ref(false)
|
|
|
|
|
+const permissionSaving = ref(false)
|
|
|
|
|
+const permissionCategory = ref('')
|
|
|
|
|
+const permissionTab = ref('user')
|
|
|
|
|
+const permissionUser = ref(null)
|
|
|
|
|
+const permissionPoints = ref([])
|
|
|
|
|
+const permissionResources = ref([])
|
|
|
|
|
+const selectedPermissionCodes = ref([])
|
|
|
|
|
+const effectivePermissionCodes = ref([])
|
|
|
|
|
+const resourceSavingId = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+const hasAnyUserOperationPermission = computed(() => {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'action.user.changeCount',
|
|
|
|
|
+ 'action.user.setSendCountAutoApprove',
|
|
|
|
|
+ 'action.user.ban',
|
|
|
|
|
+ 'action.user.permissionManage'
|
|
|
|
|
+ ].some(code => hasPermission(code))
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const filteredPermissionPoints = computed(() => {
|
|
|
|
|
+ if (!permissionCategory.value) return permissionPoints.value
|
|
|
|
|
+ return permissionPoints.value.filter(item => item.category === permissionCategory.value)
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
const isSendCountAutoApprove = (record) => Number(record?.send_count_auto_approve) === 1
|
|
const isSendCountAutoApprove = (record) => Number(record?.send_count_auto_approve) === 1
|
|
|
const isBanned = (record) => Number(record?.is_banned) === 1
|
|
const isBanned = (record) => Number(record?.is_banned) === 1
|
|
@@ -246,6 +352,118 @@ const onBanChange = async (record, checked) => {
|
|
|
await setUserBan(record, 0)
|
|
await setUserBan(record, 0)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const openPermissionManage = async (record) => {
|
|
|
|
|
+ if (!record?.uuid) return
|
|
|
|
|
+ permissionUser.value = record
|
|
|
|
|
+ permissionVisible.value = true
|
|
|
|
|
+ permissionLoading.value = true
|
|
|
|
|
+ permissionTab.value = 'user'
|
|
|
|
|
+ permissionCategory.value = ''
|
|
|
|
|
+ selectedPermissionCodes.value = []
|
|
|
|
|
+ effectivePermissionCodes.value = []
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [pointRes, userRes, resourceRes] = await Promise.all([
|
|
|
|
|
+ getPermissionPoints(),
|
|
|
|
|
+ getUserPermissions({ userid: record.uuid }),
|
|
|
|
|
+ getPermissionResources()
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ if (!pointRes || pointRes.code !== 0)
|
|
|
|
|
+ throw new Error(pointRes?.msg || '获取权限点失败')
|
|
|
|
|
+ if (!userRes || userRes.code !== 0)
|
|
|
|
|
+ throw new Error(userRes?.msg || '获取用户权限失败')
|
|
|
|
|
+ if (!resourceRes || resourceRes.code !== 0)
|
|
|
|
|
+ throw new Error(resourceRes?.msg || '获取权限规则失败')
|
|
|
|
|
+
|
|
|
|
|
+ permissionPoints.value = pointRes.data || []
|
|
|
|
|
+ permissionResources.value = (resourceRes.data || []).map(item => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ enabled: Number(item.enabled) === 0 ? 0 : 1,
|
|
|
|
|
+ required_codes: item.required_codes || []
|
|
|
|
|
+ }))
|
|
|
|
|
+ selectedPermissionCodes.value = buildSelectedPermissionCodes({
|
|
|
|
|
+ directPermissionCodes: userRes.data?.directPermissionCodes,
|
|
|
|
|
+ deniedBasicPermissionCodes: userRes.data?.deniedBasicPermissionCodes
|
|
|
|
|
+ })
|
|
|
|
|
+ effectivePermissionCodes.value = userRes.data?.effectivePermissionCodes || []
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '获取权限失败',
|
|
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ permissionVisible.value = false
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ permissionLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formatResourceRule = (rule) => {
|
|
|
|
|
+ if (rule.resource_type === 'page') return `页面:${rule.resource_key}`
|
|
|
|
|
+ if (rule.resource_type === 'action') return `操作:${rule.resource_key}`
|
|
|
|
|
+ if (rule.resource_type === 'api') return `接口:${rule.api_method || ''} ${rule.api_path || rule.resource_key}`
|
|
|
|
|
+ return rule.resource_key
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const saveResourceRule = async (rule) => {
|
|
|
|
|
+ resourceSavingId.value = rule.id
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await updatePermissionResource({
|
|
|
|
|
+ id: rule.id,
|
|
|
|
|
+ enabled: rule.enabled,
|
|
|
|
|
+ required_codes: rule.required_codes || []
|
|
|
|
|
+ })
|
|
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '保存权限规则失败',
|
|
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ Message.success('权限规则已保存')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '保存权限规则失败',
|
|
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ resourceSavingId.value = 0
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handlePermissionBeforeOk = async (done) => {
|
|
|
|
|
+ if (!permissionUser.value?.uuid) return false
|
|
|
|
|
+ permissionSaving.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await setUserPermissions({
|
|
|
|
|
+ userid: permissionUser.value.uuid,
|
|
|
|
|
+ permissionCodes: filterSavablePermissionCodes(selectedPermissionCodes.value),
|
|
|
|
|
+ deniedBasicPermissionCodes: extractDeniedBasicPermissionCodes(selectedPermissionCodes.value)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '保存权限失败',
|
|
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ effectivePermissionCodes.value = res.data?.effectivePermissionCodes || []
|
|
|
|
|
+ Message.success('权限已保存')
|
|
|
|
|
+ done()
|
|
|
|
|
+ return true
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ Notification.error({
|
|
|
|
|
+ title: '保存权限失败',
|
|
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
|
|
+ })
|
|
|
|
|
+ return false
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ permissionSaving.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const columns = [
|
|
const columns = [
|
|
|
|
|
|
|
|
{
|
|
{
|
|
@@ -287,22 +505,14 @@ const columns = [
|
|
|
title: '剩余乐跑次数',
|
|
title: '剩余乐跑次数',
|
|
|
dataIndex: 'lepao_count',
|
|
dataIndex: 'lepao_count',
|
|
|
width: 120,
|
|
width: 120,
|
|
|
- }, {
|
|
|
|
|
- title: '赠送免审',
|
|
|
|
|
- slotName: 'send_count_auto_approve',
|
|
|
|
|
- width: 110,
|
|
|
|
|
}, {
|
|
}, {
|
|
|
title: '账号状态',
|
|
title: '账号状态',
|
|
|
slotName: 'is_banned_status',
|
|
slotName: 'is_banned_status',
|
|
|
width: 100,
|
|
width: 100,
|
|
|
- }, {
|
|
|
|
|
- title: '封禁',
|
|
|
|
|
- slotName: 'is_banned',
|
|
|
|
|
- width: 80,
|
|
|
|
|
}, {
|
|
}, {
|
|
|
title: '操作',
|
|
title: '操作',
|
|
|
slotName: 'optional',
|
|
slotName: 'optional',
|
|
|
- width: 100,
|
|
|
|
|
|
|
+ width: 120,
|
|
|
align: 'center',
|
|
align: 'center',
|
|
|
fixed: 'right'
|
|
fixed: 'right'
|
|
|
}]
|
|
}]
|
|
@@ -437,4 +647,40 @@ const formatLastLoginType = (type) => {
|
|
|
.muted {
|
|
.muted {
|
|
|
color: var(--color-text-3);
|
|
color: var(--color-text-3);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.permission-list {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-height: 420px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-title {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: var(--color-text-1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-code,
|
|
|
|
|
+.permission-remark {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ color: var(--color-text-3);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-section-title {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-card-basic {
|
|
|
|
|
+ opacity: 0.85;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-resource-list {
|
|
|
|
|
+ max-height: 480px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.permission-resource-card + .permission-resource-card {
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|