feat(product): 新增产品管理模块与字典组件功能
- 新增产品管理相关路由和页面(dashboard、list、requirement、setting) - 实现产品基础信息编辑弹窗组件(base-info-dialog.vue) - 添加运行时字典功能(dict-select、dict-text、dict-tag组件) - 集成字典管理store和API调用 - 规范ID类型定义为string避免精度丢失问题 - 完善国际化资源文件支持中英文对照 - 新增对象上下文业务域入口页导航实现说明 - 添加Vue DevTools浮动入口注释说明 - 统一权限控制支持全局和对象作用域区分 - 规范分页查询参数类型定义与使用方式
This commit is contained in:
249
src/views/system/role/modules/role-context-panel.vue
Normal file
249
src/views/system/role/modules/role-context-panel.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { objectTypeRecord, scopeTypeRecord } from '@/constants/business';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'RoleContextPanel' });
|
||||
|
||||
interface Props {
|
||||
total?: number;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
total: 0,
|
||||
loading: false
|
||||
});
|
||||
|
||||
const scopeType = defineModel<Api.SystemManage.ScopeType>('scopeType', {
|
||||
required: true
|
||||
});
|
||||
|
||||
const objectType = defineModel<Api.SystemManage.ObjectType | undefined>('objectType');
|
||||
|
||||
const isObjectScope = computed(() => scopeType.value === 'object');
|
||||
const scopeOptions = computed(() => [
|
||||
{ label: $t(scopeTypeRecord.global), value: 'global' satisfies Api.SystemManage.ScopeType },
|
||||
{ label: $t(scopeTypeRecord.object), value: 'object' satisfies Api.SystemManage.ScopeType }
|
||||
]);
|
||||
|
||||
const objectTypeOptions = computed(() => [
|
||||
{ label: $t(objectTypeRecord.product), value: 'product' satisfies Api.SystemManage.ObjectType },
|
||||
{ label: $t(objectTypeRecord.project), value: 'project' satisfies Api.SystemManage.ObjectType }
|
||||
]);
|
||||
|
||||
const currentContextLabel = computed(() => {
|
||||
if (!isObjectScope.value) {
|
||||
return $t(scopeTypeRecord.global);
|
||||
}
|
||||
|
||||
if (!objectType.value) {
|
||||
return `${$t(scopeTypeRecord.object)} / --`;
|
||||
}
|
||||
|
||||
return `${$t(scopeTypeRecord.object)} / ${$t(objectTypeRecord[objectType.value])}`;
|
||||
});
|
||||
|
||||
const currentScopeSummary = computed(() => {
|
||||
if (!isObjectScope.value) {
|
||||
return $t('page.system.role.globalRoleSummary');
|
||||
}
|
||||
|
||||
if (objectType.value === 'product') {
|
||||
return $t('page.system.role.objectRoleSummaryProduct');
|
||||
}
|
||||
|
||||
if (objectType.value === 'project') {
|
||||
return $t('page.system.role.objectRoleSummaryProject');
|
||||
}
|
||||
|
||||
return $t('page.system.role.objectRoleSummary');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="role-context-panel" body-class="role-context-panel__body">
|
||||
<div v-loading="props.loading" class="role-context-panel__layout">
|
||||
<div class="role-context-panel__controls">
|
||||
<div class="role-context-panel__field role-context-panel__field--switch">
|
||||
<ElSegmented v-model="scopeType" :options="scopeOptions" />
|
||||
</div>
|
||||
|
||||
<span v-if="isObjectScope" class="role-context-panel__divider" aria-hidden="true">|</span>
|
||||
|
||||
<div v-if="isObjectScope" class="role-context-panel__field role-context-panel__field--inline">
|
||||
<span class="role-context-panel__field-label role-context-panel__field-label--inline">
|
||||
{{ $t('page.system.menu.objectType') }}
|
||||
</span>
|
||||
<ElSelect v-model="objectType" class="w-full" :placeholder="$t('page.system.menu.objectTypePlaceholder')">
|
||||
<ElOption v-for="item in objectTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="role-context-panel__info">
|
||||
<div class="role-context-panel__info-main">
|
||||
<div class="role-context-panel__info-item">
|
||||
<span class="role-context-panel__info-label">{{ $t('page.system.menu.currentContext') }}</span>
|
||||
<strong class="role-context-panel__info-value">{{ currentContextLabel }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="role-context-panel__info-item">
|
||||
<span class="role-context-panel__info-label">{{ $t('page.system.role.currentRoleCount') }}</span>
|
||||
<strong class="role-context-panel__info-value">{{ props.total }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="role-context-panel__info-desc">{{ currentScopeSummary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.role-context-panel {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
background: var(--el-fill-color-blank);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.role-context-panel__body) {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.role-context-panel__layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.role-context-panel__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.role-context-panel__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.role-context-panel__field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.role-context-panel__field--switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.role-context-panel__field--switch .el-segmented) {
|
||||
width: auto;
|
||||
padding: 6px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.role-context-panel__field--switch .el-segmented__item) {
|
||||
min-height: 40px;
|
||||
min-width: 96px;
|
||||
padding: 0 22px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-context-panel__field-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-context-panel__field--inline {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.role-context-panel__field-label--inline {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.role-context-panel__divider {
|
||||
color: var(--el-border-color-darker);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(.role-context-panel__field--inline .el-select) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.role-context-panel__info-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.role-context-panel__info-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.role-context-panel__info-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-context-panel__info-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-context-panel__info-desc {
|
||||
margin-top: 10px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.role-context-panel__layout {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.role-context-panel__controls {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.role-context-panel__info {
|
||||
padding-left: 0;
|
||||
padding-top: 14px;
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.role-context-panel__controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.role-context-panel__info-item {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,12 +11,14 @@ defineOptions({ name: 'RoleOperateDialog' });
|
||||
interface Props {
|
||||
operateType: UI.TableOperateType;
|
||||
rowData?: Api.SystemManage.Role | null;
|
||||
scopeType: Api.SystemManage.ScopeType;
|
||||
objectType?: Api.SystemManage.ObjectType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted', roleId: number): void;
|
||||
(e: 'submitted', roleId: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
@@ -49,12 +51,27 @@ function createDefaultModel(): Model {
|
||||
return {
|
||||
name: '',
|
||||
code: '',
|
||||
scopeType: props.scopeType,
|
||||
objectType: props.scopeType === 'object' ? props.objectType : undefined,
|
||||
sort: 0,
|
||||
status: 0,
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentScopeParams(): Api.SystemManage.ScopeQueryParams {
|
||||
if (props.scopeType === 'object') {
|
||||
return {
|
||||
scopeType: 'object',
|
||||
objectType: props.objectType
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scopeType: 'global'
|
||||
};
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: createRequiredRule($t('page.system.role.form.roleName')),
|
||||
code: createRequiredRule($t('page.system.role.form.roleCode')),
|
||||
@@ -85,6 +102,8 @@ async function initModel() {
|
||||
model.value = {
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
scopeType: data.scopeType ?? props.scopeType,
|
||||
objectType: data.objectType || (props.scopeType === 'object' ? props.objectType : undefined),
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark ?? ''
|
||||
@@ -102,26 +121,35 @@ async function handleSubmit() {
|
||||
|
||||
const submitData: Api.SystemManage.SaveRoleParams = {
|
||||
...model.value,
|
||||
...getCurrentScopeParams(),
|
||||
name: model.value.name.trim(),
|
||||
code: model.value.code.trim(),
|
||||
remark: model.value.remark?.trim() || null
|
||||
};
|
||||
|
||||
const request =
|
||||
isEdit.value && props.rowData
|
||||
? fetchUpdateRole({ id: props.rowData.id, ...submitData })
|
||||
: fetchCreateRole(submitData);
|
||||
let roleId = props.rowData?.id ?? '';
|
||||
|
||||
const { error, data } = await request;
|
||||
if (isEdit.value && props.rowData) {
|
||||
const { scopeType: _scopeType, objectType: _objectType, ...updateData } = submitData;
|
||||
const { error } = await fetchUpdateRole({ id: props.rowData.id, ...updateData });
|
||||
|
||||
submitting.value = false;
|
||||
submitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { error, data } = await fetchCreateRole(submitData);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleId = data;
|
||||
}
|
||||
|
||||
const roleId = isEdit.value && props.rowData ? props.rowData.id : Number(data);
|
||||
|
||||
window.$message?.success($t(isEdit.value ? 'common.updateSuccess' : 'common.addSuccess'));
|
||||
|
||||
closeModal();
|
||||
@@ -142,7 +170,6 @@ watch(visible, value => {
|
||||
preset="md"
|
||||
:loading="detailLoading"
|
||||
:confirm-loading="submitting"
|
||||
:scrollbar="false"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
|
||||
@@ -25,7 +25,7 @@ const treeRef = ref<TreeInstance | null>(null);
|
||||
const permissionLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const filterKeyword = ref('');
|
||||
const checkedKeys = ref<number[]>([]);
|
||||
const checkedKeys = ref<string[]>([]);
|
||||
|
||||
const disabled = computed(() => !props.role || props.role.status === 1);
|
||||
const checkedCount = computed(() => checkedKeys.value.length);
|
||||
@@ -37,7 +37,7 @@ const treeProps = {
|
||||
label: 'name'
|
||||
} as const;
|
||||
|
||||
function applyCheckedKeys(keys: number[]) {
|
||||
function applyCheckedKeys(keys: string[]) {
|
||||
checkedKeys.value = [...keys];
|
||||
treeRef.value?.setCheckedKeys(keys);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ function filterNode(value: string, data: any) {
|
||||
}
|
||||
|
||||
function collectExpandableNodeIds(nodes: Api.SystemManage.MenuSimple[]) {
|
||||
const ids: number[] = [];
|
||||
const ids: string[] = [];
|
||||
|
||||
const walk = (items: Api.SystemManage.MenuSimple[]) => {
|
||||
items.forEach(item => {
|
||||
@@ -112,7 +112,7 @@ async function loadRoleMenus() {
|
||||
}
|
||||
|
||||
function handleCheck() {
|
||||
checkedKeys.value = (treeRef.value?.getCheckedKeys(false) as number[]) ?? [];
|
||||
checkedKeys.value = (treeRef.value?.getCheckedKeys(false) as string[]) ?? [];
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -120,7 +120,7 @@ async function handleSave() {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuIds = (treeRef.value?.getCheckedKeys(false) as number[]) ?? [];
|
||||
const menuIds = (treeRef.value?.getCheckedKeys(false) as string[]) ?? [];
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user