Files
wordpress-theme-aksrt/assets/admin/src/pages/permissions/PermissionsCapabilities.vue
T
kate522 b4d04ae346 feat(users): medal auto-grant + aksrt_get_user_name chrome
Add AKSRT_User_Badges::auto_evaluate on login for get_type metrics,
introduce aksrt_get_user_name (auth→wear medal→level→follow order),
wire single author bar/card, and document transfer/withdraw default-closed
semantics on capabilities page. audit errors=0.
2026-07-17 01:12:09 +08:00

252 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="aksrt-page-container">
<PageHeader
title="基本权限 / 能力矩阵"
desc="统一管理用户治理、投稿和评论的角色能力矩阵。"
>
<template #actions>
<n-space>
<n-button quaternary @click="resetDefaults">恢复默认</n-button>
</n-space>
</template>
</PageHeader>
<n-alert type="warning" :show-icon="true" style="margin: 12px 0 16px;">
转账/提现(balance_transfer、points_transfer、balance_withdraw)默认 <b>exclude all</b>,与 Zibll 一致:未在矩阵中为角色/等级/VIP/认证打开前,前台入口会隐藏或拒绝。
medal_manually_set 控制谁可授予徽章;管理员 manage_options 始终可通过。
</n-alert>
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
<n-card class="aksrt-section-card" v-if="summary.cap_count">
<div class="aksrt-section-header">
<h3>矩阵概览</h3>
</div>
<n-space>
<n-tag round type="info">能力项:{{ summary.cap_count }}</n-tag>
<n-tag round type="info">分组:{{ summary.group_count }}</n-tag>
<n-tag round type="info">角色列:{{ summary.column_count }}</n-tag>
</n-space>
</n-card>
<n-card v-for="group in groups" :key="group.key" class="aksrt-section-card">
<template #header>
<div class="aksrt-cap-matrix__header">
<div>
<h3 class="aksrt-cap-matrix__title">{{ group.title }}</h3>
<p class="aksrt-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
</div>
</div>
</template>
<n-data-table
:columns="buildColumns(group)"
:data="group.items"
:bordered="true"
size="small"
:single-line="false"
:scroll-x="1200"
/>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存能力矩阵</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { h, onMounted, reactive, ref } from 'vue'
import { NInputNumber, NSwitch, NTag } from 'naive-ui'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const groups = ref([])
const columnsMeta = ref({})
const summary = reactive({
cap_count: 0,
group_count: 0,
column_count: 0,
})
const userCaps = reactive({})
const defaultsSnapshot = ref({})
onMounted(load)
async function load() {
loading.value = true
try {
const res = await api.getCapabilities()
const data = res?.data || {}
columnsMeta.value = data.columns || {}
groups.value = data.groups || []
Object.assign(summary, data.summary || { cap_count: 0, group_count: 0, column_count: 0 })
const snapshot = {}
for (const group of groups.value) {
for (const item of group.items || []) {
snapshot[item.cap_id] = { ...(item.values || {}) }
}
}
replaceReactive(userCaps, snapshot)
defaultsSnapshot.value = JSON.parse(JSON.stringify(snapshot))
} catch (error) {
message.error(error?.message || '加载能力矩阵失败')
} finally {
loading.value = false
}
}
function replaceReactive(target, source) {
Object.keys(target).forEach((key) => delete target[key])
Object.entries(source).forEach(([key, value]) => {
target[key] = { ...value }
})
}
function buildColumns(group) {
const cols = [
{
title: '能力项',
key: 'label',
fixed: 'left',
width: 280,
render: (row) => h('div', { class: 'aksrt-cap-matrix__cap' }, [
h('strong', row.label),
h('code', row.cap_id),
]),
},
]
const visibleColumns = new Set()
for (const item of group.items || []) {
for (const col of item.visible_columns || []) {
visibleColumns.add(col)
}
}
for (const col of Object.keys(columnsMeta.value || {})) {
if (!visibleColumns.has(col)) continue
cols.push({
title: columnsMeta.value[col],
key: col,
width: col === 'level' || col === 'vip' ? 110 : 100,
render: (row) => renderCapCell(row, col),
})
}
return cols
}
function renderCapCell(row, col) {
const hidden = !(row.visible_columns || []).includes(col)
if (hidden) {
return h('span', { class: 'aksrt-cap-matrix__empty' }, '—')
}
if (!userCaps[row.cap_id]) {
userCaps[row.cap_id] = { ...(row.values || {}) }
}
if (col === 'level' || col === 'vip') {
return h(NInputNumber, {
value: userCaps[row.cap_id]?.[col] ?? -1,
min: -1,
max: col === 'level' ? 10 : 2,
step: 1,
size: 'small',
style: 'width: 90px',
onUpdateValue: (value) => updateCapValue(row.cap_id, col, value ?? -1),
})
}
return h(NSwitch, {
value: !!userCaps[row.cap_id]?.[col],
size: 'small',
onUpdateValue: (value) => updateCapValue(row.cap_id, col, value),
})
}
function updateCapValue(capId, col, value) {
if (!userCaps[capId]) {
userCaps[capId] = {}
}
userCaps[capId][col] = value
}
function resetDefaults() {
replaceReactive(userCaps, JSON.parse(JSON.stringify(defaultsSnapshot.value || {})))
message.info('已恢复默认矩阵')
}
function expandAll() {
message.info('当前矩阵为平铺展示,无需展开')
}
function collapseAll() {
message.info('当前矩阵为平铺展示,无需折叠')
}
async function handleSave() {
saving.value = true
try {
await api.saveCapabilities(JSON.parse(JSON.stringify(userCaps)))
defaultsSnapshot.value = JSON.parse(JSON.stringify(userCaps))
message.success('能力矩阵已保存')
} catch (error) {
message.error(error?.message || '保存能力矩阵失败')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.aksrt-cap-matrix__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.aksrt-cap-matrix__title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.aksrt-cap-matrix__desc {
margin: 4px 0 0;
font-size: 12px;
color: #8a909c;
}
.aksrt-cap-matrix__cap {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-cap-matrix__cap code {
font-size: 11px;
color: #667085;
}
.aksrt-cap-matrix__empty {
color: #c0c4cc;
}
.aksrt-form-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>