Files
shrimp-server/examples/basic-admin/web/src/module/role.vue
T
wc24 27a2348815 demo: 加 Vue 前端到 examples/basic-admin/web
为了端到端验证 Shrimp 框架,把 hand-admin 前端拷进 examples/basic-admin/web
作为临时演示用前端,@shrimp alias 直接指向同仓 src/,免构建。

修改要点:
- web/vite.config.ts: @shrimp 改成 ../../../src; proxy /admin-api 到 :3100
- web/tsconfig.json: 同步 @shrimp 路径; 删掉 hand-server-2 的 paths
- web/src/locales: 删掉 matter_*/zerobase 业务 key
- web/src/module: 删掉 matter_*/zerobase 业务 vue,只留 user/role/FieldRelativeTime
- examples/basic-admin/index.ts:
  - mount 到 /admin-api/* 而不是 /api/*
  - 加 /config /login /me 端点 (mock super-admin operator,固定 token)
  - DemoShrimp 子类覆盖 getMenuTree 用模块自身 label
  - MemoryShrimpCrud 的 getRelatedCrud 手动 wire 到 crudRegistry,
    populate 才能跑 (后续 70→90 改进项)

curl 已验证 /config /login /me /product/list 全通,tagIds_label 正确旁挂。
2026-05-23 15:28:10 +08:00

239 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>
<el-container class="role-page">
<!-- 左侧:角色列表 -->
<el-aside width="260px">
<div class="role-list-card">
<div class="role-list-header">{{ $t('role.menuList') }}</div>
<div class="role-list-body">
<div v-for="role in roleList" :key="role._id" class="role-item"
:class="{ active: activeId === role._id }" @click="onSelect(role._id)">
{{ role.name }}
</div>
<div v-if="roleList.length === 0" class="empty-tip">{{ $t('role.empty') }}</div>
</div>
<div class="role-list-footer" v-if="can('add')">
<el-button type="primary" @click="handleAdd" class="add-btn">{{ $t('role.addRole') }}</el-button>
</div>
</div>
</el-aside>
<!-- 右侧:角色编辑 -->
<el-main>
<div v-if="currentRole" class="role-form">
<el-form :model="formData" label-width="80px">
<el-form-item :label="$t('role.name')">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item :label="$t('role.description')">
<el-input v-model="formData.description" type="textarea" :rows="3" />
</el-form-item>
<el-form-item :label="$t('role.powers')">
<PowerTree :modelValue="formData.powers || []" :allPowers="allPowers" :menuTree="vueAdmin.menuTree"
@update:modelValue="val => formData.powers = val" />
</el-form-item>
</el-form>
<div class="form-footer">
<el-button v-if="can('edit')" type="primary" @click="handleSave">{{ $t('base.save') }}</el-button>
<el-popconfirm v-if="can('delete')" :title="$t('role.confirmDelete')" :confirm-button-text="$t('base.delete')"
:cancel-button-text="$t('base.cancel')" confirm-button-type="danger" @confirm="handleDelete">
<template #reference>
<el-button type="danger">{{ $t('base.delete') }}</el-button>
</template>
</el-popconfirm>
</div>
</div>
<el-empty v-else :description="$t('role.selectOrCreate')" />
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { vueAdmin } from '@/VueAdmin'
import { IRole, IRoleProto, WithId, ILabel } from '@shrimp/IShrimpType'
import { ElMessage } from 'element-plus'
import PowerTree from '@/components/PowerTree.vue'
interface IPowerItem {
key: string
label: ILabel
}
const { t } = useI18n()
const props = defineProps<{
moduleKey: string
events?: string[]
}>()
const roleApi = vueAdmin.getModule<IRoleProto>('role')
const roleList = ref<WithId<IRole>[]>([])
const activeId = ref('')
const allPowers = ref<IPowerItem[]>([])
const currentRole = ref<WithId<IRole> | null>(null)
const formData = ref<Partial<Omit<IRole, 'createdAt' | 'updatedAt'>>>({
name: '',
description: '',
powers: [],
})
const can = vueAdmin.can('role')
async function loadRoles() {
const res = await roleApi.list()
roleList.value = res.list || res as any
}
function onSelect(id: string) {
activeId.value = id
const role = roleList.value.find(r => r._id === id)
if (role) {
currentRole.value = { ...role }
formData.value = {
name: role.name,
description: role.description,
powers: [...(role.powers || [])],
}
}
}
async function handleAdd() {
try {
const created = await roleApi.add({ name: t('role.newRole'), description: '', powers: [] } as any)
await loadRoles()
onSelect(created._id)
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
async function handleSave() {
try {
await roleApi.edit(currentRole.value!._id, formData.value)
await loadRoles()
onSelect(currentRole.value!._id)
ElMessage.success(t('base.success'))
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
async function handleDelete() {
try {
await roleApi.delete(currentRole.value!._id)
currentRole.value = null
activeId.value = ''
await loadRoles()
if (roleList.value.length > 0) {
onSelect(roleList.value[0]._id)
}
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
onMounted(async () => {
allPowers.value = await roleApi.allPower()
await loadRoles()
if (roleList.value.length > 0) {
onSelect(roleList.value[0]._id)
}
})
</script>
<style scoped>
.role-page {
height: 100%;
padding: 16px;
}
.el-aside {
margin-right: 16px;
border: none;
}
@media (max-width: 992px) {
.el-aside {
margin-right: 0;
}
.role-list-card {
margin-bottom: 16px;
}
}
/* 角色列表卡片 */
.role-list-card {
border: 1px solid var(--el-border-color);
border-radius: 8px;
background: var(--el-bg-color);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.role-list-header {
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid var(--el-border-color);
}
.role-list-body {
flex: 1;
overflow-y: auto;
}
.role-item {
padding: 10px 16px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.role-item:hover {
background: var(--el-fill-color-light);
}
.role-item.active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
.role-list-footer {
border-top: 1px solid var(--el-border-color);
}
.add-btn {
width: 100%;
height: 40px;
border-radius: 0 0 8px 8px;
border: none !important;
font-size: 14px;
}
/* 角色表单 */
.role-form {
max-width: 700px;
}
.form-footer {
padding-top: 16px;
border-top: 1px solid var(--el-border-color);
margin-top: 16px;
}
.empty-tip {
padding: 20px;
text-align: center;
color: var(--el-text-color-placeholder);
font-size: 14px;
}
</style>