导入 AKSRT 主题开发源码

This commit is contained in:
Claude Code committed 2026-07-03 22:50:34 +08:00
1 parent 9681dd3d48
commit 1268a55dd0
595 files changed
+127705

No files matched your search

@@ -0,0 +1,245 @@
<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-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>