RoleList.vue 8.45 KB
<template>
  <div>
    <a-card title="角色管理" :bordered="false" class="list-table-card">
      <div class="list-toolbar">
        <div class="list-toolbar-left">
          <a-input-search v-model:value="keyword" placeholder="搜索角色名称/编码" class="list-search" />
        </div>
        <div class="list-toolbar-right">
          <a-button type="primary" @click="openAdd">新增角色</a-button>
        </div>
      </div>

      <a-table :dataSource="filteredList" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'name'">
            <div class="role-name-cell">
              <span>{{ record.name }}</span>
              <a-tag v-if="record.builtIn" color="purple">内置</a-tag>
            </div>
          </template>

          <template v-else-if="column.key === 'roleScope'">
            <a-tag :color="record.roleScope === 'PLATFORM' ? 'blue' : 'cyan'">
              {{ formatScope(record.roleScope) }}
            </a-tag>
          </template>

          <template v-else-if="column.key === 'status'">
            <a-tag :color="record.status === 1 ? 'green' : 'red'">
              {{ record.status === 1 ? '正常' : '禁用' }}
            </a-tag>
          </template>

          <template v-else-if="column.key === 'binding'">
            <span class="binding-copy">{{ formatBinding(record) }}</span>
          </template>

          <template v-else-if="column.key === 'action'">
            <a-space wrap>
              <a v-if="!record.builtIn" @click="openEdit(record)">编辑</a>
              <a-tooltip v-else title="内置角色不允许编辑">
                <span class="action-disabled">编辑</span>
              </a-tooltip>

              <a v-if="record.status === 1" @click="goAssignMenus(record)">分配菜单</a>
              <a-tooltip v-else title="请先启用角色再分配菜单">
                <span class="action-disabled">分配菜单</span>
              </a-tooltip>

              <a-popconfirm
                v-if="canToggleStatus(record)"
                :title="record.status === 1 ? '确认禁用?' : '确认启用?'"
                @confirm="toggleStatus(record)"
              >
                <a :style="record.status === 1 ? 'color:red' : ''">
                  {{ record.status === 1 ? '禁用' : '启用' }}
                </a>
              </a-popconfirm>
              <a-tooltip v-else :title="getToggleBlockedReason(record)">
                <span class="action-disabled">{{ record.status === 1 ? '禁用' : '启用' }}</span>
              </a-tooltip>

              <a-popconfirm v-if="canDelete(record)" title="确认删除?" @confirm="handleDel(record.id)">
                <a style="color:red">删除</a>
              </a-popconfirm>
              <a-tooltip v-else :title="getDeleteBlockedReason(record)">
                <span class="action-disabled">删除</span>
              </a-tooltip>
            </a-space>
          </template>
        </template>
      </a-table>
    </a-card>

    <a-modal
      v-model:open="modalVisible"
      :title="editingId ? '编辑角色' : '新增角色'"
      @ok="handleSave"
      :confirmLoading="saving"
    >
      <div class="soft-page-stack">
        <div class="soft-note-card">
          <strong>角色说明</strong>
          <p>角色只控制菜单显示,不做完整权限点授权。创建后可继续进入“分配菜单”为角色配置可见菜单。</p>
        </div>
        <a-form :model="form" layout="vertical">
          <a-form-item label="角色名称">
            <a-input v-model:value="form.name" />
          </a-form-item>
          <a-form-item label="角色编码">
            <a-input v-model:value="form.code" />
          </a-form-item>
          <a-form-item label="角色范围">
            <a-select v-model:value="form.roleScope" :disabled="!!editingId">
              <a-select-option value="PLATFORM">平台角色</a-select-option>
              <a-select-option value="SUBSTATION">分站角色</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </div>
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { systemRoleApi } from '@/api'

interface RoleItem {
  id: number
  code: string
  name: string
  roleScope: string
  status: number
  builtIn?: boolean
  adminUserCount?: number
  substationCount?: number
  createTime?: number
}

const router = useRouter()
const loading = ref(false)
const saving = ref(false)
const keyword = ref('')
const list = ref<RoleItem[]>([])
const modalVisible = ref(false)
const editingId = ref<number | null>(null)
const form = reactive({ name: '', code: '', roleScope: 'PLATFORM' })

const columns = [
  { title: 'ID', dataIndex: 'id', width: 80 },
  { title: '角色名称', key: 'name' },
  { title: '角色编码', dataIndex: 'code' },
  { title: '角色范围', key: 'roleScope', width: 110 },
  { title: '状态', key: 'status', width: 100 },
  { title: '绑定情况', key: 'binding' },
  { title: '操作', key: 'action', width: 320 },
]

const filteredList = computed(() => {
  const value = keyword.value.trim().toLowerCase()
  if (!value) return list.value
  return list.value.filter(item =>
    item.name.toLowerCase().includes(value) || item.code.toLowerCase().includes(value),
  )
})

function formatScope(scope: string) {
  return scope === 'PLATFORM' ? '平台' : '分站'
}

function formatBinding(record: RoleItem) {
  const parts: string[] = []
  if ((record.adminUserCount || 0) > 0) parts.push(`平台账号 ${record.adminUserCount}`)
  if ((record.substationCount || 0) > 0) parts.push(`分站账号 ${record.substationCount}`)
  return parts.length ? parts.join(' / ') : '未绑定账号'
}

function isBound(record: RoleItem) {
  return (record.adminUserCount || 0) > 0 || (record.substationCount || 0) > 0
}

function canToggleStatus(record: RoleItem) {
  if (record.builtIn) return false
  if (record.status === 1 && isBound(record)) return false
  return true
}

function canDelete(record: RoleItem) {
  return !record.builtIn && !isBound(record)
}

function getToggleBlockedReason(record: RoleItem) {
  if (record.builtIn) return '内置角色不允许禁用'
  if (record.status === 1 && isBound(record)) return '角色已绑定账号,不能禁用'
  return '当前角色不可操作'
}

function getDeleteBlockedReason(record: RoleItem) {
  if (record.builtIn) return '内置角色不允许删除'
  if (isBound(record)) return '角色已绑定账号,不能删除'
  return '当前角色不可删除'
}

async function loadList() {
  loading.value = true
  try {
    const res: any = await systemRoleApi.list(true)
    list.value = Array.isArray(res?.data) ? res.data : []
  } finally {
    loading.value = false
  }
}

function openAdd() {
  editingId.value = null
  Object.assign(form, { name: '', code: '', roleScope: 'PLATFORM' })
  modalVisible.value = true
}

function openEdit(record: RoleItem) {
  editingId.value = record.id
  Object.assign(form, { name: record.name, code: record.code, roleScope: record.roleScope })
  modalVisible.value = true
}

async function handleSave() {
  if (!form.name.trim() || !form.code.trim()) {
    message.error('请填写角色名称和编码')
    return
  }
  saving.value = true
  try {
    const payload = { name: form.name.trim(), code: form.code.trim(), roleScope: form.roleScope }
    if (editingId.value) {
      await systemRoleApi.edit({ id: editingId.value, ...payload })
    } else {
      await systemRoleApi.add(payload)
    }
    message.success('保存成功')
    modalVisible.value = false
    await loadList()
  } finally {
    saving.value = false
  }
}

async function toggleStatus(record: RoleItem) {
  if (record.status === 1) {
    await systemRoleApi.ban(record.id)
  } else {
    await systemRoleApi.cancelBan(record.id)
  }
  message.success('操作成功')
  await loadList()
}

async function handleDel(id: number) {
  await systemRoleApi.del(id)
  message.success('删除成功')
  await loadList()
}

function goAssignMenus(record: RoleItem) {
  router.push({ path: '/system/role-menu', query: { roleId: String(record.id) } })
}

onMounted(loadList)
</script>

<style scoped>
.role-name-cell {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.binding-copy {
  color: var(--text-soft);
}

.action-disabled {
  color: var(--text-soft);
  cursor: not-allowed;
}
</style>