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:
2026-04-23 09:05:55 +08:00
parent c5911ea34b
commit 4122dfa50d
95 changed files with 9581 additions and 801 deletions

View File

@@ -1,10 +1,10 @@
<script setup lang="tsx">
import {computed, nextTick, onMounted, reactive, ref, watch} from 'vue';
import type {TableInstance} from 'element-plus';
import {ElButton, ElPopconfirm, ElSwitch, ElTag} from 'element-plus';
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import type { TableInstance } from 'element-plus';
import { ElButton, ElPopconfirm, ElSwitch, ElTag } from 'element-plus';
import dayjs from 'dayjs';
import type {FlatResponseData} from '@sa/axios';
import {userGenderRecord} from '@/constants/business';
import type { FlatResponseData } from '@sa/axios';
import { userGenderRecord } from '@/constants/business';
import {
fetchBatchDeleteUser,
fetchDeleteDept,
@@ -18,11 +18,11 @@ import {
fetchUpdateUser,
fetchUpdateUserStatus
} from '@/service/api';
import {useUIPaginatedTable} from '@/hooks/common/table';
import { useUIPaginatedTable } from '@/hooks/common/table';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import {$t} from '@/locales';
import {buildMenuTree} from '@/views/system/shared/menu-tree';
import { $t } from '@/locales';
import { buildMenuTree } from '@/views/system/shared/menu-tree';
import UserManagementRelation from '@/views/system/user-management-relation/index.vue';
import UserOperateDialog from './modules/user-operate-dialog.vue';
import UserOrgLeaderDialog from './modules/user-org-leader-dialog.vue';
@@ -32,7 +32,7 @@ import UserResignedDialog from './modules/user-resigned-dialog.vue';
import UserResetPasswordDialog from './modules/user-reset-password-dialog.vue';
import UserSearch from './modules/user-search.vue';
defineOptions({name: 'UserManage'});
defineOptions({ name: 'UserManage' });
function getInitSearchParams(): Api.SystemManage.UserSearchParams {
return {
@@ -158,7 +158,7 @@ const deptTree = computed(() => buildMenuTree(deptList.value));
const currentDept = computed(() => deptList.value.find(item => item.id === currentDeptId.value) ?? null);
const deptCount = computed(() => deptList.value.length);
const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} = useUIPaginatedTable<
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
FlatResponseData<any, Api.SystemManage.UserList>,
Api.SystemManage.User
>({
@@ -182,9 +182,9 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
searchParams.pageSize = params.pageSize ?? 10;
},
columns: () => [
{prop: 'selection', type: 'selection', width: 48},
{prop: 'index', type: 'index', label: $t('common.index'), width: 64},
{prop: 'username', label: $t('page.system.user.userName'), minWidth: 140, showOverflowTooltip: true},
{ prop: 'selection', type: 'selection', width: 48 },
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
{ prop: 'username', label: $t('page.system.user.userName'), minWidth: 140, showOverflowTooltip: true },
{
prop: 'nickname',
label: $t('page.system.user.nickName'),
@@ -274,9 +274,9 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
formatter: row => {
const state = getUserResignedState(row);
const stateMap: Record<UserResignedState, { type: UI.ThemeColor; label: App.I18n.I18nKey }> = {
active: {type: 'success', label: 'page.system.user.resignedStateEnum.active'},
pending: {type: 'warning', label: 'page.system.user.resignedStateEnum.pending'},
resigned: {type: 'info', label: 'page.system.user.resignedStateEnum.resigned'}
active: { type: 'success', label: 'page.system.user.resignedStateEnum.active' },
pending: { type: 'warning', label: 'page.system.user.resignedStateEnum.pending' },
resigned: { type: 'info', label: 'page.system.user.resignedStateEnum.resigned' }
};
return <ElTag type={stateMap[state].type}>{$t(stateMap[state].label)}</ElTag>;
@@ -337,7 +337,7 @@ const {columns, columnChecks, data, loading, getDataByPage, mobilePagination} =
async function loadDeptTree() {
deptLoading.value = true;
const {error, data: deptItems} = await fetchGetDeptList({
const { error, data: deptItems } = await fetchGetDeptList({
status: 0
});
@@ -452,7 +452,7 @@ function openOrgLeader(row: Api.SystemManage.Dept) {
}
async function handleDeleteDeptAction(row: Api.SystemManage.Dept) {
const {error} = await fetchDeleteDept(row.id);
const { error } = await fetchDeleteDept(row.id);
if (error) {
return;
@@ -477,7 +477,7 @@ async function handleDeleteAction(row: Api.SystemManage.User) {
return;
}
const {error} = await fetchDeleteUser(row.id);
const { error } = await fetchDeleteUser(row.id);
if (error) {
return;
@@ -496,7 +496,7 @@ async function updateUserResignedAt(userId: number, value: number | null) {
const user = detailResult.data;
const {error} = await fetchUpdateUser({
const { error } = await fetchUpdateUser({
id: userId,
username: user.username,
nickname: user.nickname ?? null,
@@ -548,7 +548,7 @@ async function handleBatchDelete() {
return;
}
const {error} = await fetchBatchDeleteUser(userCheckedRowKeys.value);
const { error } = await fetchBatchDeleteUser(userCheckedRowKeys.value);
if (error) {
return;
@@ -561,7 +561,7 @@ async function handleBatchDelete() {
async function handleToggleStatus(row: Api.SystemManage.User, enabled: boolean) {
statusLoadingIds.value = [...statusLoadingIds.value, row.id];
const {error} = await fetchUpdateUserStatus({
const { error } = await fetchUpdateUserStatus({
id: row.id,
status: enabled ? 0 : 1
});
@@ -671,13 +671,13 @@ onMounted(async () => {
<template #default>
<ElButton plain type="primary" :disabled="!currentDept" @click="openAdd">
<template #icon>
<icon-ic-round-plus class="text-icon"/>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</ElButton>
<ElButton plain type="primary" :disabled="!currentDept" @click="userManagementRelationVisible = true">
<template #icon>
<icon-ic-round-plus class="text-icon"/>
<icon-ic-round-plus class="text-icon" />
</template>
管理链路
</ElButton>
@@ -685,7 +685,7 @@ onMounted(async () => {
<template #reference>
<ElButton type="danger" plain :disabled="userCheckedRowKeys.length === 0">
<template #icon>
<icon-ic-round-delete class="text-icon"/>
<icon-ic-round-delete class="text-icon" />
</template>
{{ $t('common.batchDelete') }}
</ElButton>
@@ -707,7 +707,7 @@ onMounted(async () => {
:data="data"
@selection-change="handleUserSelectionChange"
>
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col"/>
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
</ElTable>
</div>
<div class="mt-20px flex justify-end">
@@ -722,7 +722,7 @@ onMounted(async () => {
</template>
<div v-else class="h-full flex items-center justify-center">
<ElEmpty :description="$t('page.system.user.emptyOrg')"/>
<ElEmpty :description="$t('page.system.user.emptyOrg')" />
</div>
</ElCard>
</div>
@@ -763,7 +763,7 @@ onMounted(async () => {
@submitted="handleDeptSubmitted"
/>
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData"/>
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData" />
<BusinessFormDialog
v-model="userManagementRelationVisible"
@@ -772,7 +772,7 @@ onMounted(async () => {
:show-footer="false"
max-body-height="70vh"
>
<UserManagementRelation :from-user-index="true" :dept-id="currentDeptId" :org-type="currentDept?.orgType"/>
<UserManagementRelation :from-user-index="true" :dept-id="currentDeptId" :org-type="currentDept?.orgType" />
</BusinessFormDialog>
</div>
</template>

View File

@@ -56,7 +56,7 @@ const title = computed(() => {
const isEdit = computed(() => props.operateType === 'edit');
type Model = Api.SystemManage.SaveUserParams & {
roleIds: number[];
roleIds: string[];
};
const model = ref<Model>(createDefaultModel());

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import {computed, nextTick, ref, watch} from 'vue';
import {commonStatusOptions} from '@/constants/business';
import {fetchCreateDept, fetchUpdateDept} from '@/service/api';
import {useForm, useFormRules} from '@/hooks/common/form';
import { computed, nextTick, ref, watch } from 'vue';
import { commonStatusOptions } from '@/constants/business';
import { fetchCreateDept, fetchUpdateDept } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import {$t} from '@/locales';
import { $t } from '@/locales';
defineOptions({name: 'UserOrgOperateDialog'});
defineOptions({ name: 'UserOrgOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
@@ -28,8 +28,8 @@ const visible = defineModel<boolean>('visible', {
default: false
});
const {formRef, validate} = useForm();
const {createRequiredRule} = useFormRules();
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const submitting = ref(false);
@@ -44,10 +44,10 @@ const title = computed(() => {
});
const orgTypeOptions: CommonType.Option<Api.SystemManage.DeptOrgType, App.I18n.I18nKey>[] = [
{value: 'company', label: 'page.system.user.orgType.company'},
{value: 'dept', label: 'page.system.user.orgType.dept'},
{value: 'direction', label: 'page.system.user.orgType.direction'},
{value: 'team', label: 'page.system.user.orgType.team'}
{ value: 'company', label: 'page.system.user.orgType.company' },
{ value: 'dept', label: 'page.system.user.orgType.dept' },
{ value: 'direction', label: 'page.system.user.orgType.direction' },
{ value: 'team', label: 'page.system.user.orgType.team' }
];
type Model = Api.SystemManage.SaveDeptParams;
@@ -149,7 +149,7 @@ async function handleSubmit() {
} as Api.SystemManage.SaveDeptParams;
if (isEdit.value && props.rowData) {
const {error} = await fetchUpdateDept({
const { error } = await fetchUpdateDept({
id: props.rowData.id,
...payload
});
@@ -166,7 +166,7 @@ async function handleSubmit() {
return;
}
const {error, data} = await fetchCreateDept(payload);
const { error, data } = await fetchCreateDept(payload);
submitting.value = false;
@@ -203,7 +203,7 @@ watch(visible, async value => {
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.orgName')" prop="name">
<ElInput v-model="model.name" :placeholder="$t('page.system.user.form.orgName')"/>
<ElInput v-model="model.name" :placeholder="$t('page.system.user.form.orgName')" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
@@ -222,7 +222,7 @@ watch(visible, async value => {
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.orgTypeLabel')" prop="orgType">
<ElSelect v-model="model.orgType" :placeholder="$t('page.system.user.form.orgTypeLabel')">
<ElOption v-for="item in orgTypeOptions" :key="item.value" :label="$t(item.label)" :value="item.value"/>
<ElOption v-for="item in orgTypeOptions" :key="item.value" :label="$t(item.label)" :value="item.value" />
</ElSelect>
</ElFormItem>
</ElCol>
@@ -236,7 +236,7 @@ watch(visible, async value => {
:value="item.value"
/>
</ElSelect>
<ElInput v-else v-model="model.code" :placeholder="$t('page.system.user.form.orgCode')"/>
<ElInput v-else v-model="model.code" :placeholder="$t('page.system.user.form.orgCode')" />
</ElFormItem>
</ElCol>
<ElCol :span="12">

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { SYSTEM_USER_COMPANY_DICT_CODE } from '@/constants/dict';
import { commonStatusOptions } from '@/constants/business';
import { translateOptions } from '@/utils/common';
import DictSelect from '@/components/custom/dict-select.vue';
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
import { $t } from '@/locales';
@@ -8,7 +10,6 @@ defineOptions({ name: 'UserSearch' });
interface Props {
roleOptions: Api.SystemManage.RoleSimple[];
companyOptions: Api.Dict.DictData[];
disabled?: boolean;
}
@@ -75,15 +76,13 @@ const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required
</ElCol>
<ElCol :lg="6" :md="8" :sm="12">
<ElFormItem label="所属公司" prop="company">
<ElSelect
<DictSelect
v-model="model.company"
clearable
:dict-code="SYSTEM_USER_COMPANY_DICT_CODE"
filterable
:disabled="disabled"
placeholder="请选择所属公司"
>
<ElOption v-for="item in companyOptions" :key="item.value" :label="item.label" :value="item.value" />
</ElSelect>
/>
</ElFormItem>
</ElCol>
<ElCol :lg="6" :md="8" :sm="12">