| | |
| | | } |
| | | function fetchGetMenuList() { |
| | | return request.get({ |
| | | url: '/auth/menu/v2' |
| | | url: '/auth/menu' |
| | | }) |
| | | } |
| | | export { |
| | |
| | | ...(params.phone !== undefined ? { phone: params.phone } : {}), |
| | | ...(params.email !== undefined ? { email: params.email } : {}), |
| | | ...(params.status !== undefined ? { status: params.status } : {}), |
| | | ...(params.deptId !== undefined ? { deptId: params.deptId } : {}) |
| | | ...(params.deptId !== undefined ? { deptId: params.deptId } : {}), |
| | | ...(params.code !== undefined ? { code: params.code } : {}), |
| | | ...(params.sex !== undefined ? { sex: params.sex } : {}), |
| | | ...(params.realName !== undefined ? { realName: params.realName } : {}), |
| | | ...(params.idCard !== undefined ? { idCard: params.idCard } : {}), |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}), |
| | | ...(params.condition !== undefined ? { condition: params.condition } : {}), |
| | | ...(params.roleIds !== undefined ? { roleIds: params.roleIds } : {}) |
| | | } |
| | | } |
| | | |
| | |
| | | ...(params.status !== undefined ? { status: params.status } : {}), |
| | | ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}), |
| | | ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}), |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}) |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}), |
| | | ...(params.orderBy !== undefined ? { orderBy: params.orderBy } : {}) |
| | | } |
| | | } |
| | | |
| | |
| | | ...(params.sort !== undefined ? { sort: params.sort } : {}), |
| | | ...(params.group !== undefined ? { group: params.group } : {}), |
| | | ...(params.status !== undefined ? { status: params.status } : {}), |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}) |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}), |
| | | ...(params.orderBy !== undefined ? { orderBy: params.orderBy } : {}) |
| | | } |
| | | } |
| | | |
| | |
| | | return request.get({ url: `/user/${id}` }) |
| | | } |
| | | |
| | | async function fetchExportUserReport(payload = {}, options = {}) { |
| | | return fetch(`${import.meta.env.VITE_API_URL}/user/export`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...(options.headers || {}) |
| | | }, |
| | | body: JSON.stringify(payload) |
| | | }) |
| | | } |
| | | |
| | | function fetchGetRoleList(params) { |
| | | return request.post({ url: '/role/page', params: buildRoleListParams(params) }) |
| | | } |
| | |
| | | return request.get({ url: `/dictType/${id}` }) |
| | | } |
| | | |
| | | function fetchGetDictDataDetail(id) { |
| | | return request.get({ url: `/dictData/${id}` }) |
| | | } |
| | | |
| | | function fetchSaveDictType(params) { |
| | | return request.post({ url: '/dictType/save', params }) |
| | | } |
| | | |
| | | function fetchSaveDictData(params) { |
| | | return request.post({ url: '/dictData/save', params }) |
| | | } |
| | | |
| | | function fetchUpdateDictType(params) { |
| | | return request.post({ url: '/dictType/update', params }) |
| | | } |
| | | |
| | | function fetchUpdateDictData(params) { |
| | | return request.post({ url: '/dictData/update', params }) |
| | | } |
| | | |
| | | function fetchDeleteDictType(id) { |
| | | return request.post({ url: `/dictType/remove/${id}` }) |
| | | } |
| | | |
| | | function fetchDeleteDictData(id) { |
| | | return request.post({ url: `/dictData/remove/${id}` }) |
| | | } |
| | | |
| | | async function fetchExportDictTypeReport(payload = {}, options = {}) { |
| | | return fetch(`${import.meta.env.VITE_API_URL}/dictType/export`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...(options.headers || {}) |
| | | }, |
| | | body: JSON.stringify(payload) |
| | | }) |
| | | } |
| | | |
| | | async function fetchExportDictDataReport(payload = {}, options = {}) { |
| | | return fetch(`${import.meta.env.VITE_API_URL}/dictData/export`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...(options.headers || {}) |
| | | }, |
| | | body: JSON.stringify(payload) |
| | | }) |
| | | } |
| | | |
| | | function fetchWaveRulePage(params = {}) { |
| | |
| | | fetchResetUserPassword, |
| | | fetchUpdateUserStatus, |
| | | fetchGetUserDetail, |
| | | fetchExportUserReport, |
| | | fetchGetRoleList, |
| | | fetchOperationRecordPage, |
| | | fetchGetOperationRecordDetail, |
| | |
| | | fetchDeleteSerialRule, |
| | | fetchDictTypePage, |
| | | fetchGetDictTypeDetail, |
| | | fetchGetDictDataDetail, |
| | | fetchSaveDictType, |
| | | fetchSaveDictData, |
| | | fetchUpdateDictType, |
| | | fetchUpdateDictData, |
| | | fetchDeleteDictType, |
| | | fetchDeleteDictData, |
| | | fetchExportDictTypeReport, |
| | | fetchExportDictDataReport, |
| | | fetchDictDataPage, |
| | | fetchWaveRulePage, |
| | | fetchGetWaveRuleDetail, |
| New file |
| | |
| | | import { $t } from '@/locales' |
| | | |
| | | const DEFAULT_DICT_DATA_ORDER_BY = 'sort asc' |
| | | |
| | | function hasValue(value) { |
| | | return value !== '' && value !== void 0 && value !== null |
| | | } |
| | | |
| | | function normalizeText(value) { |
| | | return String(value ?? '').trim() |
| | | } |
| | | |
| | | function normalizeNumber(value, fallback = 0) { |
| | | if (!hasValue(value)) { |
| | | return fallback |
| | | } |
| | | const normalized = Number(value) |
| | | return Number.isNaN(normalized) ? fallback : normalized |
| | | } |
| | | |
| | | export function createDictDataSearchState(dictTypeData = {}) { |
| | | return { |
| | | condition: '', |
| | | dictTypeId: hasValue(dictTypeData.id) ? normalizeNumber(dictTypeData.id, 0) : '', |
| | | dictTypeCode: normalizeText(dictTypeData.code), |
| | | value: '', |
| | | label: '', |
| | | sort: '', |
| | | memo: '', |
| | | status: '', |
| | | orderBy: DEFAULT_DICT_DATA_ORDER_BY |
| | | } |
| | | } |
| | | |
| | | export function createDictDataFormState(dictTypeData = {}) { |
| | | return { |
| | | id: null, |
| | | dictTypeId: hasValue(dictTypeData.id) ? normalizeNumber(dictTypeData.id, 0) : 0, |
| | | dictTypeCode: normalizeText(dictTypeData.code), |
| | | value: '', |
| | | label: '', |
| | | group: '', |
| | | sort: 0, |
| | | status: 1, |
| | | memo: '' |
| | | } |
| | | } |
| | | |
| | | export function getDictDataPaginationKey() { |
| | | return { |
| | | current: 'current', |
| | | size: 'pageSize' |
| | | } |
| | | } |
| | | |
| | | export function getDictDataStatusOptions() { |
| | | return [ |
| | | { label: $t('common.status.normal'), value: 1 }, |
| | | { label: $t('common.status.frozen'), value: 0 } |
| | | ] |
| | | } |
| | | |
| | | export function getDictDataStatusMeta(status, t = $t) { |
| | | return Number(status) === 1 |
| | | ? { text: t('common.status.normal'), type: 'success', bool: true } |
| | | : { text: t('common.status.frozen'), type: 'danger', bool: false } |
| | | } |
| | | |
| | | export function buildDictDataSearchParams(params = {}) { |
| | | const searchParams = { |
| | | condition: normalizeText(params.condition), |
| | | dictTypeId: hasValue(params.dictTypeId) ? normalizeNumber(params.dictTypeId, 0) : '', |
| | | dictTypeCode: normalizeText(params.dictTypeCode), |
| | | value: normalizeText(params.value), |
| | | label: normalizeText(params.label), |
| | | sort: normalizeText(params.sort), |
| | | memo: normalizeText(params.memo), |
| | | status: params.status, |
| | | orderBy: normalizeText(params.orderBy) || DEFAULT_DICT_DATA_ORDER_BY |
| | | } |
| | | |
| | | return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value))) |
| | | } |
| | | |
| | | export function buildDictDataPageQueryParams(params = {}) { |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | ...buildDictDataSearchParams(params) |
| | | } |
| | | } |
| | | |
| | | export function buildDictDataDialogModel(record = {}, dictTypeData = {}) { |
| | | return { |
| | | ...createDictDataFormState(dictTypeData), |
| | | ...(record.id ? { id: normalizeNumber(record.id, 0) } : {}), |
| | | dictTypeId: hasValue(record.dictTypeId) |
| | | ? normalizeNumber(record.dictTypeId, 0) |
| | | : createDictDataFormState(dictTypeData).dictTypeId, |
| | | dictTypeCode: |
| | | normalizeText(record.dictTypeCode) || createDictDataFormState(dictTypeData).dictTypeCode, |
| | | value: normalizeText(record.value), |
| | | label: normalizeText(record.label), |
| | | group: normalizeText(record.group), |
| | | sort: normalizeNumber(record.sort, 0), |
| | | status: hasValue(record.status) ? normalizeNumber(record.status, 1) : 1, |
| | | memo: normalizeText(record.memo) |
| | | } |
| | | } |
| | | |
| | | export function buildDictDataSavePayload(formData = {}, dictTypeData = {}) { |
| | | const baseForm = createDictDataFormState(dictTypeData) |
| | | return { |
| | | ...(formData.id ? { id: normalizeNumber(formData.id, 0) } : {}), |
| | | dictTypeId: hasValue(formData.dictTypeId) |
| | | ? normalizeNumber(formData.dictTypeId, 0) |
| | | : baseForm.dictTypeId, |
| | | dictTypeCode: normalizeText(formData.dictTypeCode) || baseForm.dictTypeCode, |
| | | value: normalizeText(formData.value), |
| | | label: normalizeText(formData.label), |
| | | group: normalizeText(formData.group), |
| | | sort: normalizeNumber(formData.sort, 0), |
| | | status: hasValue(formData.status) ? normalizeNumber(formData.status, 1) : 1, |
| | | memo: normalizeText(formData.memo) |
| | | } |
| | | } |
| | | |
| | | export function normalizeDictDataListRow(record = {}, t = $t) { |
| | | const statusMeta = getDictDataStatusMeta(record.status, t) |
| | | return { |
| | | ...record, |
| | | dictTypeId: hasValue(record.dictTypeId) ? normalizeNumber(record.dictTypeId, 0) : 0, |
| | | dictTypeCode: normalizeText(record.dictTypeCode), |
| | | value: normalizeText(record.value), |
| | | label: normalizeText(record.label), |
| | | group: normalizeText(record.group), |
| | | sort: normalizeNumber(record.sort, 0), |
| | | memo: normalizeText(record.memo), |
| | | statusText: record['status$'] || statusMeta.text, |
| | | statusType: statusMeta.type, |
| | | statusBool: record.statusBool ?? statusMeta.bool, |
| | | updateByLabel: record['updateBy$'] || normalizeText(record.updateByLabel), |
| | | createByLabel: record['createBy$'] || normalizeText(record.createByLabel), |
| | | updateTimeText: record['updateTime$'] || normalizeText(record.updateTime), |
| | | createTimeText: record['createTime$'] || normalizeText(record.createTime) |
| | | } |
| | | } |
| New file |
| | |
| | | import { h } from 'vue' |
| | | import { ElTag } from 'element-plus' |
| | | import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue' |
| | | import { $t } from '@/locales' |
| | | |
| | | export function createDictDataTableColumns({ handleEdit, handleDelete, t = $t }) { |
| | | return [ |
| | | { |
| | | prop: 'dictTypeCode', |
| | | label: '字典编码', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'value', |
| | | label: '值', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'label', |
| | | label: '标签', |
| | | minWidth: 160, |
| | | showOverflowTooltip: true |
| | | }, |
| | | { |
| | | prop: 'group', |
| | | label: '分组', |
| | | minWidth: 140, |
| | | showOverflowTooltip: true, |
| | | formatter: (row) => row.group || t('common.placeholder.empty') |
| | | }, |
| | | { |
| | | prop: 'sort', |
| | | label: t('table.sort'), |
| | | width: 90 |
| | | }, |
| | | { |
| | | prop: 'status', |
| | | label: t('table.status'), |
| | | width: 100, |
| | | formatter: (row) => |
| | | h( |
| | | ElTag, |
| | | { type: row.statusType, effect: 'light' }, |
| | | () => row.statusText || t('common.placeholder.empty') |
| | | ) |
| | | }, |
| | | { |
| | | prop: 'updateByLabel', |
| | | label: t('table.updateBy'), |
| | | width: 120, |
| | | formatter: (row) => row.updateByLabel || t('common.placeholder.empty') |
| | | }, |
| | | { |
| | | prop: 'updateTimeText', |
| | | label: t('table.updateTime'), |
| | | minWidth: 180, |
| | | formatter: (row) => row.updateTimeText || t('common.placeholder.empty') |
| | | }, |
| | | { |
| | | prop: 'memo', |
| | | label: t('table.memo'), |
| | | minWidth: 180, |
| | | showOverflowTooltip: true, |
| | | formatter: (row) => row.memo || t('common.placeholder.empty') |
| | | }, |
| | | { |
| | | prop: 'operation', |
| | | label: t('table.operation'), |
| | | width: handleDelete ? 120 : 80, |
| | | align: 'center', |
| | | formatter: (row) => { |
| | | const buttons = [ |
| | | h(ArtButtonTable, { |
| | | type: 'edit', |
| | | onClick: () => handleEdit(row) |
| | | }) |
| | | ] |
| | | |
| | | if (handleDelete) { |
| | | buttons.push( |
| | | h(ArtButtonTable, { |
| | | type: 'delete', |
| | | onClick: () => handleDelete(row) |
| | | }) |
| | | ) |
| | | } |
| | | |
| | | return h('div', { class: 'flex justify-center' }, buttons) |
| | | } |
| | | } |
| | | ] |
| | | } |
| | |
| | | import { $t } from '@/locales' |
| | | |
| | | const DEFAULT_DICT_TYPE_ORDER_BY = 'create_time desc' |
| | | |
| | | export function createDictTypeSearchState() { |
| | | return { |
| | | condition: '', |
| | | code: '', |
| | | name: '', |
| | | status: '' |
| | | description: '', |
| | | memo: '', |
| | | timeStart: '', |
| | | timeEnd: '', |
| | | status: '', |
| | | orderBy: DEFAULT_DICT_TYPE_ORDER_BY |
| | | } |
| | | } |
| | | |
| | |
| | | condition: String(params.condition || '').trim(), |
| | | code: String(params.code || '').trim(), |
| | | name: String(params.name || '').trim(), |
| | | description: String(params.description || '').trim(), |
| | | memo: String(params.memo || '').trim(), |
| | | timeStart: String(params.timeStart || '').trim(), |
| | | timeEnd: String(params.timeEnd || '').trim(), |
| | | orderBy: String(params.orderBy || '').trim() || DEFAULT_DICT_TYPE_ORDER_BY, |
| | | ...(params.status !== '' && params.status !== null && params.status !== undefined |
| | | ? { status: Number(params.status) } |
| | | : {}) |
| | |
| | | import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue' |
| | | import { $t } from '@/locales' |
| | | |
| | | export function createDictTypeTableColumns({ handleView, handleEdit, handleDelete, t = $t }) { |
| | | export function createDictTypeTableColumns({ |
| | | handleView, |
| | | handleEdit, |
| | | handleDelete, |
| | | handleManageData, |
| | | t = $t |
| | | }) { |
| | | const hasStandaloneView = !handleManageData && handleView |
| | | |
| | | return [ |
| | | { |
| | | prop: 'code', |
| | |
| | | label: t('table.status'), |
| | | width: 100, |
| | | formatter: (row) => |
| | | h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || t('common.placeholder.empty')) |
| | | h( |
| | | ElTag, |
| | | { type: row.statusType, effect: 'light' }, |
| | | () => row.statusText || t('common.placeholder.empty') |
| | | ) |
| | | }, |
| | | { |
| | | prop: 'updateByLabel', |
| | |
| | | { |
| | | prop: 'operation', |
| | | label: t('table.operation'), |
| | | width: handleDelete ? 160 : 120, |
| | | width: handleDelete ? (hasStandaloneView ? 200 : 240) : hasStandaloneView ? 160 : 200, |
| | | align: 'center', |
| | | formatter: (row) => { |
| | | const buttons = [ |
| | | const buttons = [] |
| | | |
| | | if (handleManageData) { |
| | | buttons.push( |
| | | h(ArtButtonTable, { |
| | | type: 'view', |
| | | onClick: () => handleManageData(row) |
| | | }) |
| | | ) |
| | | } |
| | | |
| | | if (hasStandaloneView) { |
| | | buttons.push( |
| | | h(ArtButtonTable, { |
| | | type: 'view', |
| | | onClick: () => handleView(row) |
| | | }), |
| | | }) |
| | | ) |
| | | } |
| | | |
| | | buttons.push( |
| | | h(ArtButtonTable, { |
| | | type: 'edit', |
| | | onClick: () => handleEdit(row) |
| | | }) |
| | | ] |
| | | ) |
| | | |
| | | if (handleDelete) { |
| | | buttons.push( |
| | |
| | | ) |
| | | } |
| | | |
| | | return h('div', { class: 'flex justify-center' }, buttons) |
| | | return h('div', { class: 'flex items-center justify-center gap-2' }, buttons) |
| | | } |
| | | } |
| | | ] |
| | |
| | | <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> |
| | | <template #left> |
| | | <ElSpace wrap> |
| | | <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>{{ t('pages.system.dictType.buttons.add') }}</ElButton> |
| | | <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>{{ |
| | | t('pages.system.dictType.buttons.add') |
| | | }}</ElButton> |
| | | <ElButton |
| | | v-auth="'delete'" |
| | | type="danger" |
| | |
| | | v-ripple |
| | | > |
| | | {{ t('common.actions.batchDelete') }} |
| | | </ElButton> |
| | | <ElButton |
| | | v-auth="'query'" |
| | | :loading="exportLoading" |
| | | :disabled="loading || exportLoading" |
| | | @click="handleExport" |
| | | v-ripple |
| | | > |
| | | {{ t('common.actions.export') }} |
| | | </ElButton> |
| | | </ElSpace> |
| | | </template> |
| | |
| | | :loading="detailLoading" |
| | | :detail-data="detailData" |
| | | /> |
| | | |
| | | <DictDataPanel |
| | | v-model:visible="dictDataPanelVisible" |
| | | :dict-type-data="currentManagedDictTypeData" |
| | | /> |
| | | </ElCard> |
| | | </div> |
| | | </template> |
| | |
| | | <script setup> |
| | | import { useI18n } from 'vue-i18n' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { useCrudPage } from '@/views/system/common/useCrudPage' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { |
| | | fetchDeleteDictType, |
| | | fetchDictTypePage, |
| | | fetchExportDictTypeReport, |
| | | fetchGetDictTypeDetail, |
| | | fetchSaveDictType, |
| | | fetchUpdateDictType |
| | | } from '@/api/system-manage' |
| | | import DictDataPanel from './modules/dict-data-panel.vue' |
| | | import DictTypeDialog from './modules/dict-type-dialog.vue' |
| | | import DictTypeDetailDrawer from './modules/dict-type-detail-drawer.vue' |
| | | import { createDictTypeTableColumns } from './dictTypeTable.columns' |
| | |
| | | |
| | | const { t } = useI18n() |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | const searchForm = ref(createDictTypeSearchState()) |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | const dictDataPanelVisible = ref(false) |
| | | const currentManagedDictTypeData = ref(buildDictTypeDialogModel()) |
| | | const exportLoading = ref(false) |
| | | let handleDeleteAction = null |
| | | |
| | | const searchItems = computed(() => [ |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: t('pages.system.dictType.table.description'), |
| | | key: 'description', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典描述' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.memo'), |
| | | key: 'memo', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入备注' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.status'), |
| | | key: 'status', |
| | | type: 'select', |
| | |
| | | { label: t('common.status.normal'), value: 1 }, |
| | | { label: t('common.status.frozen'), value: 0 } |
| | | ] |
| | | } |
| | | }, |
| | | { |
| | | label: '开始日期', |
| | | key: 'timeStart', |
| | | type: 'date', |
| | | props: { |
| | | clearable: true, |
| | | valueFormat: 'YYYY-MM-DD', |
| | | type: 'date' |
| | | } |
| | | }, |
| | | { |
| | | label: '结束日期', |
| | | key: 'timeEnd', |
| | | type: 'date', |
| | | props: { |
| | | clearable: true, |
| | | valueFormat: 'YYYY-MM-DD', |
| | | type: 'date' |
| | | } |
| | | } |
| | | ]) |
| | |
| | | } |
| | | } |
| | | |
| | | function openDictDataPanel(row) { |
| | | currentManagedDictTypeData.value = buildDictTypeDialogModel(row) |
| | | dictDataPanelVisible.value = true |
| | | } |
| | | |
| | | const { |
| | | columns, |
| | | columnChecks, |
| | |
| | | handleView: openDetail, |
| | | handleEdit: openEditDialog, |
| | | handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null, |
| | | handleManageData: hasAuth('system:dictData:list') |
| | | ? (row) => openDictDataPanel(row) |
| | | : null, |
| | | t |
| | | }) |
| | | }, |
| | |
| | | }) |
| | | handleDeleteAction = handleDelete |
| | | |
| | | async function handleExport() { |
| | | exportLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchExportDictTypeReport(buildDictTypeSearchParams(searchForm.value), { |
| | | headers: { |
| | | Authorization: userStore.accessToken || '' |
| | | } |
| | | }), |
| | | null, |
| | | { |
| | | timeoutMessage: '数据字典导出超时,已停止等待' |
| | | } |
| | | ) |
| | | if (!response) { |
| | | return |
| | | } |
| | | if (!response.ok) { |
| | | throw new Error(`导出失败,状态码:${response.status}`) |
| | | } |
| | | |
| | | const blob = await response.blob() |
| | | const downloadUrl = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = downloadUrl |
| | | link.download = 'dict-type.xlsx' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | link.remove() |
| | | window.URL.revokeObjectURL(downloadUrl) |
| | | ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '导出失败') |
| | | } finally { |
| | | exportLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleSearch(params) { |
| | | replaceSearchParams(buildDictTypeSearchParams(params)) |
| | | getData() |
| New file |
| | |
| | | <template> |
| | | <ElDialog |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | width="820px" |
| | | align-center |
| | | @update:model-value="handleVisibleUpdate" |
| | | @closed="handleClosed" |
| | | > |
| | | <ArtForm |
| | | ref="formRef" |
| | | v-model="form" |
| | | :items="formItems" |
| | | :rules="rules" |
| | | :span="12" |
| | | :gutter="20" |
| | | label-width="100px" |
| | | :show-reset="false" |
| | | :show-submit="false" |
| | | /> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton> |
| | | </span> |
| | | </template> |
| | | </ElDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ElMessageBox } from 'element-plus' |
| | | import { useI18n } from 'vue-i18n' |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | import { |
| | | buildDictDataDialogModel, |
| | | createDictDataFormState, |
| | | getDictDataStatusOptions |
| | | } from '../dictDataPage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | visible: { type: Boolean, default: false }, |
| | | dictTypeData: { type: Object, default: () => ({}) }, |
| | | dictData: { type: Object, default: () => ({}) } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const form = reactive(createDictDataFormState()) |
| | | const initialSnapshot = ref(createComparableSnapshot()) |
| | | const { t } = useI18n() |
| | | |
| | | const isEdit = computed(() => Boolean(form.id)) |
| | | const dialogTitle = computed(() => (isEdit.value ? '编辑字典项' : '新增字典项')) |
| | | |
| | | const rules = computed(() => ({ |
| | | dictTypeId: [{ required: true, message: '缺少字典类型ID', trigger: 'blur' }], |
| | | dictTypeCode: [{ required: true, message: '缺少字典类型编码', trigger: 'blur' }], |
| | | value: [{ required: true, message: '请输入字典值', trigger: 'blur' }], |
| | | label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }] |
| | | })) |
| | | |
| | | const formItems = computed(() => [ |
| | | { |
| | | label: '字典类型ID', |
| | | key: 'dictTypeId', |
| | | type: 'input', |
| | | props: { |
| | | disabled: true |
| | | } |
| | | }, |
| | | { |
| | | label: '字典编码', |
| | | key: 'dictTypeCode', |
| | | type: 'input', |
| | | props: { |
| | | disabled: true |
| | | } |
| | | }, |
| | | { |
| | | label: '字典值', |
| | | key: 'value', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典值' |
| | | } |
| | | }, |
| | | { |
| | | label: '字典标签', |
| | | key: 'label', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典标签' |
| | | } |
| | | }, |
| | | { |
| | | label: '分组', |
| | | key: 'group', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入分组' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.sort'), |
| | | key: 'sort', |
| | | type: 'number', |
| | | props: { |
| | | min: 0, |
| | | controlsPosition: 'right', |
| | | style: { width: '100%' } |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.status'), |
| | | key: 'status', |
| | | type: 'select', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请选择状态', |
| | | options: getDictDataStatusOptions() |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.memo'), |
| | | key: 'memo', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | type: 'textarea', |
| | | rows: 3, |
| | | clearable: true, |
| | | placeholder: '请输入备注' |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | function resetForm() { |
| | | Object.assign(form, createDictDataFormState(props.dictTypeData)) |
| | | initialSnapshot.value = createComparableSnapshot() |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | function loadFormData() { |
| | | const nextForm = buildDictDataDialogModel(props.dictData, props.dictTypeData) |
| | | Object.assign(form, nextForm) |
| | | initialSnapshot.value = createComparableSnapshot(nextForm) |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | if (!formRef.value) return |
| | | try { |
| | | await formRef.value.validate() |
| | | emit('submit', { ...form }) |
| | | } catch { |
| | | return |
| | | } |
| | | } |
| | | |
| | | function closeDialog() { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | async function handleCancel() { |
| | | if (!(await confirmDiscardIfDirty())) { |
| | | return |
| | | } |
| | | closeDialog() |
| | | } |
| | | |
| | | function handleVisibleUpdate(nextVisible) { |
| | | if (!nextVisible) { |
| | | handleCancel() |
| | | return |
| | | } |
| | | emit('update:visible', true) |
| | | } |
| | | |
| | | function handleClosed() { |
| | | resetForm() |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (visible) => { |
| | | if (visible) { |
| | | loadFormData() |
| | | nextTick(() => formRef.value?.clearValidate?.()) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.dictData, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.dictTypeData, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | function createComparableSnapshot(source = form) { |
| | | return JSON.stringify({ |
| | | ...createDictDataFormState(props.dictTypeData), |
| | | ...source |
| | | }) |
| | | } |
| | | |
| | | function isDirty() { |
| | | return createComparableSnapshot() !== initialSnapshot.value |
| | | } |
| | | |
| | | async function confirmDiscardIfDirty() { |
| | | if (!isDirty()) { |
| | | return true |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', { |
| | | confirmButtonText: '放弃修改', |
| | | cancelButtonText: '继续编辑', |
| | | type: 'warning' |
| | | }) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="字典项管理" |
| | | size="1100px" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | > |
| | | <div class="dict-data-panel space-y-4"> |
| | | <div class="text-sm text-[var(--art-text-secondary)]"> |
| | | 当前字典:{{ currentDictTypeLabel }} |
| | | </div> |
| | | |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | | :showExpand="false" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | /> |
| | | |
| | | <ElCard class="art-table-card"> |
| | | <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> |
| | | <template #left> |
| | | <ElSpace wrap> |
| | | <ElButton v-if="hasAuth('system:dictData:save')" @click="showDialog('add')" v-ripple> |
| | | 新增字典项 |
| | | </ElButton> |
| | | <ElButton |
| | | v-if="hasAuth('system:dictData:remove')" |
| | | type="danger" |
| | | :disabled="selectedRows.length === 0" |
| | | @click="handleBatchDelete" |
| | | v-ripple |
| | | > |
| | | {{ t('common.actions.batchDelete') }} |
| | | </ElButton> |
| | | <ElButton |
| | | v-if="hasAuth('system:dictData:list')" |
| | | :loading="exportLoading" |
| | | :disabled="loading || exportLoading" |
| | | @click="handleExport" |
| | | v-ripple |
| | | > |
| | | {{ t('common.actions.export') }} |
| | | </ElButton> |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | | |
| | | <ArtTable |
| | | :loading="loading" |
| | | :data="data" |
| | | :columns="columns" |
| | | :pagination="pagination" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination:size-change="handleSizeChange" |
| | | @pagination:current-change="handleCurrentChange" |
| | | /> |
| | | </ElCard> |
| | | |
| | | <DictDataDialog |
| | | v-model:visible="dialogVisible" |
| | | :dict-type-data="dictTypeData" |
| | | :dict-data="currentDictData" |
| | | @submit="handleDialogSubmit" |
| | | /> |
| | | </div> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useI18n } from 'vue-i18n' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { useAuth } from '@/hooks/core/useAuth' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { useCrudPage } from '@/views/system/common/useCrudPage' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { |
| | | fetchDeleteDictData, |
| | | fetchDictDataPage, |
| | | fetchExportDictDataReport, |
| | | fetchGetDictDataDetail, |
| | | fetchSaveDictData, |
| | | fetchUpdateDictData |
| | | } from '@/api/system-manage' |
| | | import DictDataDialog from './dict-data-dialog.vue' |
| | | import { createDictDataTableColumns } from '../dictDataTable.columns' |
| | | import { |
| | | buildDictDataDialogModel, |
| | | buildDictDataPageQueryParams, |
| | | buildDictDataSavePayload, |
| | | buildDictDataSearchParams, |
| | | createDictDataSearchState, |
| | | getDictDataPaginationKey, |
| | | getDictDataStatusOptions, |
| | | normalizeDictDataListRow |
| | | } from '../dictDataPage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | visible: { type: Boolean, default: false }, |
| | | dictTypeData: { type: Object, default: () => ({}) } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible']) |
| | | const { t } = useI18n() |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | const exportLoading = ref(false) |
| | | const searchForm = ref(createDictDataSearchState(props.dictTypeData)) |
| | | let handleDeleteAction = null |
| | | |
| | | const currentDictTypeLabel = computed(() => { |
| | | const code = props.dictTypeData?.code || '-' |
| | | const name = props.dictTypeData?.name || '未选择' |
| | | return `${name}(${code})` |
| | | }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | | label: t('table.keyword'), |
| | | key: 'condition', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典值/标签/备注' |
| | | } |
| | | }, |
| | | { |
| | | label: '字典值', |
| | | key: 'value', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典值' |
| | | } |
| | | }, |
| | | { |
| | | label: '字典标签', |
| | | key: 'label', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入字典标签' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.sort'), |
| | | key: 'sort', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入排序' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.memo'), |
| | | key: 'memo', |
| | | type: 'input', |
| | | props: { |
| | | clearable: true, |
| | | placeholder: '请输入备注' |
| | | } |
| | | }, |
| | | { |
| | | label: t('table.status'), |
| | | key: 'status', |
| | | type: 'select', |
| | | props: { |
| | | clearable: true, |
| | | options: getDictDataStatusOptions() |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | function buildPanelParams(extra = {}) { |
| | | return buildDictDataPageQueryParams({ |
| | | ...searchForm.value, |
| | | ...extra, |
| | | dictTypeId: props.dictTypeData?.id, |
| | | dictTypeCode: props.dictTypeData?.code |
| | | }) |
| | | } |
| | | |
| | | const { |
| | | columns, |
| | | columnChecks, |
| | | data, |
| | | loading, |
| | | pagination, |
| | | getData, |
| | | replaceSearchParams, |
| | | handleSizeChange, |
| | | handleCurrentChange, |
| | | refreshData, |
| | | refreshCreate, |
| | | refreshUpdate, |
| | | refreshRemove |
| | | } = useTable({ |
| | | core: { |
| | | apiFn: fetchDictDataPage, |
| | | apiParams: buildPanelParams(), |
| | | immediate: false, |
| | | paginationKey: getDictDataPaginationKey(), |
| | | columnsFactory: () => |
| | | createDictDataTableColumns({ |
| | | handleEdit: openEditDialog, |
| | | handleDelete: hasAuth('system:dictData:remove') |
| | | ? (row) => handleDeleteAction?.(row) |
| | | : null, |
| | | t |
| | | }) |
| | | }, |
| | | transform: { |
| | | dataTransformer: (records) => |
| | | Array.isArray(records) ? records.map((item) => normalizeDictDataListRow(item, t)) : [] |
| | | } |
| | | }) |
| | | |
| | | const { |
| | | dialogVisible, |
| | | dialogType, |
| | | currentRecord: currentDictData, |
| | | selectedRows, |
| | | handleSelectionChange, |
| | | showDialog, |
| | | handleDialogSubmit, |
| | | handleDelete, |
| | | handleBatchDelete |
| | | } = useCrudPage({ |
| | | createEmptyModel: () => buildDictDataDialogModel({}, props.dictTypeData), |
| | | buildEditModel: (record) => buildDictDataDialogModel(record, props.dictTypeData), |
| | | buildSavePayload: (formData) => buildDictDataSavePayload(formData, props.dictTypeData), |
| | | saveRequest: fetchSaveDictData, |
| | | updateRequest: fetchUpdateDictData, |
| | | deleteRequest: fetchDeleteDictData, |
| | | entityName: '字典项', |
| | | resolveRecordLabel: (record) => record?.label || record?.value || record?.id, |
| | | refreshCreate, |
| | | refreshUpdate, |
| | | refreshRemove |
| | | }) |
| | | handleDeleteAction = handleDelete |
| | | |
| | | async function openEditDialog(row) { |
| | | try { |
| | | currentDictData.value = buildDictDataDialogModel( |
| | | await fetchGetDictDataDetail(row.id), |
| | | props.dictTypeData |
| | | ) |
| | | dialogVisible.value = true |
| | | dialogType.value = 'edit' |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '获取字典项详情失败') |
| | | } |
| | | } |
| | | |
| | | async function handleExport() { |
| | | exportLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchExportDictDataReport(buildDictDataSearchParams(buildPanelParams()), { |
| | | headers: { |
| | | Authorization: userStore.accessToken || '' |
| | | } |
| | | }), |
| | | null, |
| | | { |
| | | timeoutMessage: '字典项导出超时,已停止等待' |
| | | } |
| | | ) |
| | | if (!response) { |
| | | return |
| | | } |
| | | if (!response.ok) { |
| | | throw new Error(`导出失败,状态码:${response.status}`) |
| | | } |
| | | |
| | | const blob = await response.blob() |
| | | const downloadUrl = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = downloadUrl |
| | | link.download = 'dict-data.xlsx' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | link.remove() |
| | | window.URL.revokeObjectURL(downloadUrl) |
| | | ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '导出失败') |
| | | } finally { |
| | | exportLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleSearch(params) { |
| | | searchForm.value = { |
| | | ...searchForm.value, |
| | | ...params, |
| | | dictTypeId: props.dictTypeData?.id, |
| | | dictTypeCode: props.dictTypeData?.code |
| | | } |
| | | replaceSearchParams(buildPanelParams(params)) |
| | | getData() |
| | | } |
| | | |
| | | function handleReset() { |
| | | searchForm.value = createDictDataSearchState(props.dictTypeData) |
| | | selectedRows.value = [] |
| | | replaceSearchParams(buildPanelParams(searchForm.value)) |
| | | getData() |
| | | } |
| | | |
| | | function handleVisibleChange(visible) { |
| | | emit('update:visible', visible) |
| | | } |
| | | |
| | | watch( |
| | | () => [props.visible, props.dictTypeData?.id], |
| | | ([visible, dictTypeId]) => { |
| | | if (!visible || !dictTypeId) { |
| | | return |
| | | } |
| | | searchForm.value = createDictDataSearchState(props.dictTypeData) |
| | | selectedRows.value = [] |
| | | replaceSearchParams(buildPanelParams(searchForm.value)) |
| | | getData() |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | </script> |
| | |
| | | <ElDialog |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | width="820px" |
| | | align-center |
| | | @update:model-value="handleCancel" |
| | | @update:model-value="handleVisibleUpdate" |
| | | @closed="handleClosed" |
| | | > |
| | | <ArtForm |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ElMessageBox } from 'element-plus' |
| | | import { useI18n } from 'vue-i18n' |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | import { buildDictTypeDialogModel, createDictTypeFormState } from '../dictTypePage.helpers' |
| | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const form = reactive(createDictTypeFormState()) |
| | | const initialSnapshot = ref(createComparableSnapshot()) |
| | | const { t } = useI18n() |
| | | |
| | | const isEdit = computed(() => Boolean(form.id)) |
| | | const dialogTitle = computed(() => |
| | | isEdit.value ? t('pages.system.dictType.dialog.titleEdit') : t('pages.system.dictType.dialog.titleCreate') |
| | | isEdit.value |
| | | ? t('pages.system.dictType.dialog.titleEdit') |
| | | : t('pages.system.dictType.dialog.titleCreate') |
| | | ) |
| | | |
| | | const rules = computed(() => ({ |
| | | code: [{ required: true, message: t('pages.system.dictType.validation.code'), trigger: 'blur' }], |
| | | code: [ |
| | | { required: true, message: t('pages.system.dictType.validation.code'), trigger: 'blur' } |
| | | ], |
| | | name: [{ required: true, message: t('pages.system.dictType.validation.name'), trigger: 'blur' }] |
| | | })) |
| | | |
| | |
| | | |
| | | function resetForm() { |
| | | Object.assign(form, createDictTypeFormState()) |
| | | initialSnapshot.value = createComparableSnapshot() |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | function loadFormData() { |
| | | Object.assign(form, buildDictTypeDialogModel(props.dictTypeData)) |
| | | const nextForm = buildDictTypeDialogModel(props.dictTypeData) |
| | | Object.assign(form, nextForm) |
| | | initialSnapshot.value = createComparableSnapshot(nextForm) |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | |
| | | } |
| | | } |
| | | |
| | | function handleCancel() { |
| | | function closeDialog() { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | async function handleCancel() { |
| | | if (!(await confirmDiscardIfDirty())) { |
| | | return |
| | | } |
| | | closeDialog() |
| | | } |
| | | |
| | | function handleVisibleUpdate(nextVisible) { |
| | | if (!nextVisible) { |
| | | handleCancel() |
| | | return |
| | | } |
| | | emit('update:visible', true) |
| | | } |
| | | |
| | | function handleClosed() { |
| | |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | function createComparableSnapshot(source = form) { |
| | | return JSON.stringify({ |
| | | ...createDictTypeFormState(), |
| | | ...source |
| | | }) |
| | | } |
| | | |
| | | function isDirty() { |
| | | return createComparableSnapshot() !== initialSnapshot.value |
| | | } |
| | | |
| | | async function confirmDiscardIfDirty() { |
| | | if (!isDirty()) { |
| | | return true |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', { |
| | | confirmButtonText: '放弃修改', |
| | | cancelButtonText: '继续编辑', |
| | | type: 'warning' |
| | | }) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | } |
| | | </script> |
| | |
| | | @refresh="handleRefresh" |
| | | > |
| | | <template #left> |
| | | <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>{{ t('pages.system.menu.buttons.add') }}</ElButton> |
| | | <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>{{ |
| | | t('pages.system.menu.buttons.add') |
| | | }}</ElButton> |
| | | <ElButton @click="toggleExpand" v-ripple> |
| | | {{ isExpanded ? t('pages.system.menu.actions.collapse') : t('pages.system.menu.actions.expand') }} |
| | | {{ |
| | | isExpanded |
| | | ? t('pages.system.menu.actions.collapse') |
| | | : t('pages.system.menu.actions.expand') |
| | | }} |
| | | </ElButton> |
| | | </template> |
| | | </ArtTableHeader> |
| | |
| | | function handleAddMenu() { |
| | | dialogType.value = 'menu' |
| | | editData.value = null |
| | | lockMenuType.value = true |
| | | lockMenuType.value = false |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | |
| | | function handleEditMenu(row) { |
| | | dialogType.value = 'menu' |
| | | editData.value = row |
| | | lockMenuType.value = true |
| | | lockMenuType.value = false |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | function handleEditAuth(row) { |
| | | dialogType.value = 'button' |
| | | editData.value = row |
| | | lockMenuType.value = true |
| | | lockMenuType.value = false |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | export function getMenuDisplayTitle(row = {}, titleFormatter = defaultMenuTitleFormatter) { |
| | | return titleFormatter(normalizeMenuTitleKey(row)) |
| | | const normalizedTitle = normalizeMenuTitleKey(row) |
| | | const formattedTitle = titleFormatter(normalizedTitle) |
| | | if (formattedTitle) { |
| | | return formattedTitle |
| | | } |
| | | |
| | | return defaultMenuTitleFormatter(row.name || row.meta?.title || '') |
| | | } |
| | | |
| | | export function getMenuDisplayIcon(row = {}) { |
| | |
| | | })) |
| | | } |
| | | |
| | | export function buildMenuTreeOptions(tree = [], titleFormatter = defaultMenuTitleFormatter, t = $t) { |
| | | export function buildMenuTreeOptions( |
| | | tree = [], |
| | | titleFormatter = defaultMenuTitleFormatter, |
| | | t = $t |
| | | ) { |
| | | return [ |
| | | { |
| | | label: t('table.topLevelMenu'), |
| | |
| | | }) |
| | | } |
| | | |
| | | export function filterMenuTree(items = [], filters = {}, titleFormatter = defaultMenuTitleFormatter) { |
| | | export function filterMenuTree( |
| | | items = [], |
| | | filters = {}, |
| | | titleFormatter = defaultMenuTitleFormatter |
| | | ) { |
| | | const results = [] |
| | | const searchName = String(filters.name || '').toLowerCase().trim() |
| | | const searchRoute = String(filters.route || '').toLowerCase().trim() |
| | | const searchName = String(filters.name || '') |
| | | .toLowerCase() |
| | | .trim() |
| | | const searchRoute = String(filters.route || '') |
| | | .toLowerCase() |
| | | .trim() |
| | | |
| | | for (const item of items) { |
| | | const menuTitle = getMenuDisplayTitle(item, titleFormatter).toLowerCase() |
| | |
| | | if (row.meta?.isAuthButton) { |
| | | return row.authority || row.meta?.authMark || '' |
| | | } |
| | | if (!row.meta?.authList?.length) return row.authority || '' |
| | | return t('pages.system.menu.messages.authCount', { |
| | | count: row.meta.authList.length |
| | | }) |
| | | return row.authority || '' |
| | | } |
| | | }, |
| | | { |
| | |
| | | <ElDialog |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | :close-on-click-modal="false" |
| | | @update:model-value="handleCancel" |
| | | width="760px" |
| | | align-center |
| | |
| | | const isEdit = computed(() => Boolean(form.id)) |
| | | const dialogTitle = computed(() => |
| | | form.menuType === 'button' |
| | | ? t(isEdit.value ? 'pages.system.menu.form.titleEditButton' : 'pages.system.menu.form.titleAddButton') |
| | | : t(isEdit.value ? 'pages.system.menu.form.titleEditMenu' : 'pages.system.menu.form.titleAddMenu') |
| | | ? t( |
| | | isEdit.value |
| | | ? 'pages.system.menu.form.titleEditButton' |
| | | : 'pages.system.menu.form.titleAddButton' |
| | | ) |
| | | : t( |
| | | isEdit.value |
| | | ? 'pages.system.menu.form.titleEditMenu' |
| | | : 'pages.system.menu.form.titleAddMenu' |
| | | ) |
| | | ) |
| | | const disableMenuType = computed(() => props.lockType || isEdit.value) |
| | | |
| | | const rules = computed(() => ({ |
| | | name: [{ required: true, message: form.menuType === 'button' ? t('pages.system.menu.form.validationButtonName') : t('pages.system.menu.form.validationMenuName'), trigger: 'blur' }], |
| | | name: [ |
| | | { |
| | | required: true, |
| | | message: |
| | | form.menuType === 'button' |
| | | ? t('pages.system.menu.form.validationButtonName') |
| | | : t('pages.system.menu.form.validationMenuName'), |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | route: |
| | | form.menuType === 'menu' |
| | | ? [{ required: true, message: t('pages.system.menu.form.validationRoute'), trigger: 'blur' }] |
| | | ? [ |
| | | { |
| | | required: true, |
| | | message: t('pages.system.menu.form.validationRoute'), |
| | | trigger: 'blur' |
| | | } |
| | | ] |
| | | : [], |
| | | authority: |
| | | form.menuType === 'button' |
| | | ? [{ required: true, message: t('pages.system.menu.form.validationAuthority'), trigger: 'blur' }] |
| | | ? [ |
| | | { |
| | | required: true, |
| | | message: t('pages.system.menu.form.validationAuthority'), |
| | | trigger: 'blur' |
| | | } |
| | | ] |
| | | : [] |
| | | })) |
| | | |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: form.menuType === 'button' ? t('pages.system.menu.form.nameButton') : t('pages.system.menu.form.nameMenu'), |
| | | label: |
| | | form.menuType === 'button' |
| | | ? t('pages.system.menu.form.nameButton') |
| | | : t('pages.system.menu.form.nameMenu'), |
| | | key: 'name', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: form.menuType === 'button' ? t('pages.system.menu.form.placeholderButtonName') : t('pages.system.menu.form.placeholderMenuName'), |
| | | placeholder: |
| | | form.menuType === 'button' |
| | | ? t('pages.system.menu.form.placeholderButtonName') |
| | | : t('pages.system.menu.form.placeholderMenuName'), |
| | | clearable: true |
| | | } |
| | | } |
| | |
| | | <ElDialog |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | width="720px" |
| | | align-center |
| | | @update:model-value="handleCancel" |
| | | @update:model-value="handleVisibleUpdate" |
| | | @closed="handleClosed" |
| | | > |
| | | <ArtForm |
| | |
| | | |
| | | <script setup> |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | import { buildRoleDialogModel, createRoleFormState, getRoleStatusOptions } from '../rolePage.helpers' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import { |
| | | buildRoleDialogModel, |
| | | createRoleFormState, |
| | | getRoleStatusOptions |
| | | } from '../rolePage.helpers' |
| | | import { useI18n } from 'vue-i18n' |
| | | |
| | | const props = defineProps({ |
| | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const form = reactive(createRoleFormState()) |
| | | const initialSnapshot = ref(createComparableSnapshot()) |
| | | const { t } = useI18n() |
| | | |
| | | const isEdit = computed(() => props.dialogType === 'edit') |
| | |
| | | ) |
| | | |
| | | const rules = computed(() => ({ |
| | | name: [{ required: true, message: t('pages.system.role.dialog.validationName'), trigger: 'blur' }] |
| | | name: [ |
| | | { required: true, message: t('pages.system.role.dialog.validationName'), trigger: 'blur' } |
| | | ] |
| | | })) |
| | | |
| | | function createInputFormItem(label, key, placeholder, extraProps = {}, extraConfig = {}) { |
| | |
| | | } |
| | | } |
| | | |
| | | function createSelectFormItem(label, key, placeholder, options, extraProps = {}, extraConfig = {}) { |
| | | function createSelectFormItem( |
| | | label, |
| | | key, |
| | | placeholder, |
| | | options, |
| | | extraProps = {}, |
| | | extraConfig = {} |
| | | ) { |
| | | return { |
| | | label, |
| | | key, |
| | |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form, createRoleFormState()) |
| | | initialSnapshot.value = createComparableSnapshot() |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | const loadFormData = () => { |
| | | Object.assign(form, buildRoleDialogModel(props.roleData)) |
| | | const nextForm = buildRoleDialogModel(props.roleData) |
| | | Object.assign(form, nextForm) |
| | | initialSnapshot.value = createComparableSnapshot(nextForm) |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | |
| | | } |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | const closeDialog = () => { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | const handleCancel = async () => { |
| | | if (!(await confirmDiscardIfDirty())) { |
| | | return |
| | | } |
| | | closeDialog() |
| | | } |
| | | |
| | | const handleVisibleUpdate = (nextVisible) => { |
| | | if (!nextVisible) { |
| | | handleCancel() |
| | | return |
| | | } |
| | | emit('update:visible', true) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.dialogType, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | } |
| | | ) |
| | | |
| | | function createComparableSnapshot(source = form) { |
| | | return JSON.stringify({ |
| | | ...createRoleFormState(), |
| | | ...source |
| | | }) |
| | | } |
| | | |
| | | function isDirty() { |
| | | return createComparableSnapshot() !== initialSnapshot.value |
| | | } |
| | | |
| | | async function confirmDiscardIfDirty() { |
| | | if (!isDirty()) { |
| | | return true |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', { |
| | | confirmButtonText: '放弃修改', |
| | | cancelButtonText: '继续编辑', |
| | | type: 'warning' |
| | | }) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | } |
| | | </script> |
| | |
| | | <div v-else class="space-y-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <ElSpace wrap> |
| | | <ElButton @click="handleSelectAll(config.scopeType)">{{ t('pages.system.role.permission.selectAll') }}</ElButton> |
| | | <ElButton @click="handleClear(config.scopeType)">{{ t('pages.system.role.permission.clear') }}</ElButton> |
| | | <ElButton @click="handleSelectAll(config.scopeType)">{{ |
| | | t('pages.system.role.permission.selectAll') |
| | | }}</ElButton> |
| | | <ElButton @click="handleClear(config.scopeType)">{{ |
| | | t('pages.system.role.permission.clear') |
| | | }}</ElButton> |
| | | <ElButton @click="handleToggleExpand(config.scopeType)"> |
| | | {{ |
| | | scopeState[config.scopeType].expandAll |
| | | ? t('common.actions.collapse') |
| | | : t('common.actions.expand') |
| | | }} |
| | | </ElButton> |
| | | </ElSpace> |
| | | <ElButton type="primary" @click="handleSave(config.scopeType)">{{ t('pages.system.role.permission.saveCurrent') }}</ElButton> |
| | | <ElButton type="primary" @click="handleSave(config.scopeType)">{{ |
| | | t('pages.system.role.permission.saveCurrent') |
| | | }}</ElButton> |
| | | </div> |
| | | |
| | | <div class="flex items-center gap-3"> |
| | |
| | | @clear="handleSearch(config.scopeType)" |
| | | @keyup.enter="handleSearch(config.scopeType)" |
| | | /> |
| | | <ElButton @click="handleSearch(config.scopeType)">{{ t('common.actions.search') }}</ElButton> |
| | | <ElButton @click="handleSearch(config.scopeType)">{{ |
| | | t('common.actions.search') |
| | | }}</ElButton> |
| | | </div> |
| | | |
| | | <ElScrollbar height="56vh"> |
| | | <ElTree |
| | | :key="`${config.scopeType}-${scopeState[config.scopeType].treeVersion}`" |
| | | :ref="(el) => setTreeRef(config.scopeType, el)" |
| | | :data="scopeState[config.scopeType].treeData" |
| | | node-key="id" |
| | | show-checkbox |
| | | :default-expand-all="true" |
| | | :default-expand-all="scopeState[config.scopeType].expandAll" |
| | | :default-checked-keys="scopeState[config.scopeType].checkedKeys" |
| | | :props="treeProps" |
| | | @check="handleTreeCheck(config.scopeType)" |
| | |
| | | buildRoleScopeSubmitPayload, |
| | | getRoleScopeConfig, |
| | | normalizeScopeKeys, |
| | | normalizeScopeKey, |
| | | normalizeRoleScopeTreeData |
| | | } from '../rolePage.helpers' |
| | | import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage' |
| | | import { |
| | | fetchGetRoleScopeList, |
| | | fetchGetRoleScopeTree, |
| | | fetchUpdateRoleScope |
| | | } from '@/api/system-manage' |
| | | import { resolveBackendMenuTitle } from '@/utils/backend-menu-title' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | |
| | | const emit = defineEmits(['update:visible', 'success']) |
| | | const { t } = useI18n() |
| | | |
| | | const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) => getRoleScopeConfig(scopeType)) |
| | | const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) => |
| | | getRoleScopeConfig(scopeType) |
| | | ) |
| | | const activeScopeType = ref(props.scopeType || 'menu') |
| | | const treeRefs = reactive({}) |
| | | const treeProps = { |
| | | label: 'label', |
| | | children: 'children' |
| | | } |
| | | const scopeState = reactive(Object.fromEntries(scopeConfigs.map((config) => [config.scopeType, createScopeTabState()]))) |
| | | const scopeState = reactive( |
| | | Object.fromEntries(scopeConfigs.map((config) => [config.scopeType, createScopeTabState()])) |
| | | ) |
| | | |
| | | const visible = computed({ |
| | | get: () => props.visible, |
| | | set: (value) => emit('update:visible', value) |
| | | }) |
| | | |
| | | const roleLabel = computed(() => props.roleData?.name || props.roleData?.code || t('pages.system.role.permission.unselected')) |
| | | const roleLabel = computed( |
| | | () => |
| | | props.roleData?.name || props.roleData?.code || t('pages.system.role.permission.unselected') |
| | | ) |
| | | |
| | | function createScopeTabState() { |
| | | return { |
| | |
| | | treeData: [], |
| | | checkedKeys: [], |
| | | halfCheckedKeys: [], |
| | | condition: '' |
| | | condition: '', |
| | | expandAll: true, |
| | | treeVersion: 0 |
| | | } |
| | | } |
| | | |
| | |
| | | const selectionRequest = reloadSelection |
| | | ? fetchGetRoleScopeList(config.scopeType, props.roleData.id) |
| | | : Promise.resolve(state.checkedKeys) |
| | | const treeRequest = fetchGetRoleScopeTree(config.scopeType, { condition: state.condition || '' }) |
| | | const treeRequest = fetchGetRoleScopeTree(config.scopeType, { |
| | | condition: state.condition || '' |
| | | }) |
| | | |
| | | const guardedResult = await guardRequestWithMessage( |
| | | Promise.all([selectionRequest, treeRequest]), |
| | | null, |
| | | { |
| | | timeoutMessage: t('pages.system.role.permission.scopeLoadTimeout', { title: config.title }) |
| | | timeoutMessage: t('pages.system.role.permission.scopeLoadTimeout', { |
| | | title: config.title |
| | | }) |
| | | } |
| | | ) |
| | | if (!guardedResult) { |
| | |
| | | state.treeData = normalizeRoleScopeTreeData(config.scopeType, treeData) |
| | | state.checkedKeys = normalizeScopeKeys(checkedIds) |
| | | state.halfCheckedKeys = [] |
| | | state.treeVersion += 1 |
| | | state.loaded = true |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || t('pages.system.role.permission.scopeLoadFailed', { title: config.title })) |
| | | ElMessage.error( |
| | | error?.message || t('pages.system.role.permission.scopeLoadFailed', { title: config.title }) |
| | | ) |
| | | } finally { |
| | | state.loading = false |
| | | nextTick(() => { |
| | |
| | | handleTreeCheck(scopeType) |
| | | } |
| | | |
| | | const handleToggleExpand = (scopeType) => { |
| | | const state = scopeState[scopeType] |
| | | state.expandAll = !state.expandAll |
| | | state.treeVersion += 1 |
| | | nextTick(() => { |
| | | treeRefs[scopeType]?.setCheckedKeys(state.checkedKeys) |
| | | handleTreeCheck(scopeType) |
| | | }) |
| | | } |
| | | |
| | | const handleSave = async (scopeType) => { |
| | | if (!props.roleData?.id) return |
| | | try { |
| | |
| | | } |
| | | if (activeScopeType.value === 'menu') { |
| | | const resolvedTitle = resolveBackendMenuTitle(rawLabel, data?.component || '') |
| | | return resolvedTitle ? formatMenuTitle(resolvedTitle) : '' |
| | | return resolvedTitle ? formatMenuTitle(resolvedTitle) : rawLabel |
| | | } |
| | | return rawLabel |
| | | } |
| | |
| | | } |
| | | ) |
| | | |
| | | watch( |
| | | activeScopeType, |
| | | async (scopeType) => { |
| | | watch(activeScopeType, async (scopeType) => { |
| | | if (props.visible && scopeType) { |
| | | await ensureScopeLoaded(scopeType) |
| | | } |
| | | } |
| | | ) |
| | | }) |
| | | </script> |
| | |
| | | 0: { type: 'danger', key: 'common.status.disabled', bool: false } |
| | | } |
| | | |
| | | const DEFAULT_ROLE_ORDER_BY = 'create_time asc' |
| | | |
| | | export function createRoleSearchState() { |
| | | return { |
| | | name: '', |
| | | code: '', |
| | | memo: '', |
| | | status: void 0, |
| | | condition: '' |
| | | condition: '', |
| | | orderBy: DEFAULT_ROLE_ORDER_BY |
| | | } |
| | | } |
| | | |
| | |
| | | code: normalizeText(params.code), |
| | | memo: normalizeText(params.memo), |
| | | status: params.status, |
| | | condition: normalizeText(params.condition) |
| | | condition: normalizeText(params.condition), |
| | | orderBy: normalizeText(params.orderBy) || DEFAULT_ROLE_ORDER_BY |
| | | } |
| | | |
| | | return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value))) |
| | |
| | | scopeType === 'menu' && Array.isArray(metaSource.authList) && metaSource.authList.length |
| | | ? metaSource.authList.map((auth, index) => ({ |
| | | id: normalizeScopeKey(auth.id ?? auth.authMark ?? `${node.id || 'auth'}-${index}`), |
| | | label: normalizeScopeTitle(auth.title || auth.name || auth.authMark || ''), |
| | | label: resolveScopeNodeTitle(auth), |
| | | type: 1, |
| | | isAuthButton: true, |
| | | authMark: auth.authMark || auth.authority || auth.code || '', |
| | |
| | | |
| | | return { |
| | | id: normalizeScopeKey(node.id ?? node.value), |
| | | label: normalizeScopeTitle( |
| | | node.label || node.title || node.name || metaSource.title || node.code || '' |
| | | ), |
| | | label: resolveScopeNodeTitle(node, metaSource), |
| | | type: node.type, |
| | | path: node.path || '', |
| | | component: node.component || '', |
| | |
| | | const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node |
| | | return { |
| | | id: normalizeScopeKey(node.id ?? node.value), |
| | | label: normalizeScopeTitle(node.label || node.title || node.name || metaSource.title || ''), |
| | | label: resolveScopeNodeTitle(node, metaSource), |
| | | type: 1, |
| | | isAuthButton: true, |
| | | authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '', |
| | |
| | | return trimmedTitle |
| | | } |
| | | |
| | | function resolveScopeNodeTitle(source = {}, metaSource = source) { |
| | | return normalizeScopeTitle( |
| | | source.name || |
| | | metaSource.name || |
| | | source.label || |
| | | source.title || |
| | | metaSource.title || |
| | | source.code || |
| | | metaSource.code || |
| | | source.authMark || |
| | | metaSource.authMark || |
| | | source.authority || |
| | | metaSource.authority || |
| | | '' |
| | | ) |
| | | } |
| | | |
| | | function normalizeRoleId(value) { |
| | | if (!hasValue(value)) { |
| | | return void 0 |
| | |
| | | width, |
| | | formatter: (row) => { |
| | | const statusMeta = resolveMeta(row) |
| | | return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text) |
| | | return h( |
| | | ElTag, |
| | | { type: row.statusType || statusMeta.type, effect: 'light' }, |
| | | () => row.statusText || statusMeta.text || '-' |
| | | ) |
| | | } |
| | | } |
| | | } |
| | |
| | | createTextColumn('name', $t('pages.system.role.table.name'), 140), |
| | | createTextColumn('code', $t('pages.system.role.table.code'), 140), |
| | | createTextColumn('memo', $t('pages.system.role.table.memo'), 180), |
| | | createTagColumn('status', $t('pages.system.role.table.status'), 120, (row) => getRoleStatusMeta(row.statusBool ?? row.status)), |
| | | createTagColumn('status', $t('pages.system.role.table.status'), 120, (row) => |
| | | getRoleStatusMeta(row.statusBool ?? row.status) |
| | | ), |
| | | createTextColumn('updateTimeText', $t('pages.system.role.table.updateTime'), 180, { |
| | | sortable: true, |
| | | formatter: (row) => row.updateTimeText || '-' |
| | |
| | | <template #left> |
| | | <ElSpace wrap> |
| | | <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>新增用户</ElButton> |
| | | <ElButton |
| | | v-auth="'delete'" |
| | | type="danger" |
| | | :disabled="selectedRows.length === 0" |
| | | @click="handleBatchDelete" |
| | | v-ripple |
| | | > |
| | | 批量删除 |
| | | </ElButton> |
| | | <ElButton v-auth="'query'" :loading="exportLoading" @click="handleExport" v-ripple |
| | | >导出</ElButton |
| | | > |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | |
| | | :data="data" |
| | | :columns="columns" |
| | | :pagination="pagination" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination:size-change="handleSizeChange" |
| | | @pagination:current-change="handleCurrentChange" |
| | | /> |
| | |
| | | <script setup> |
| | | import request from '@/utils/http' |
| | | import { guardRequestWithMessage } from '@/utils/sys/requestGuard' |
| | | import { useUserStore } from '@/store/modules/user' |
| | | import { |
| | | fetchDeleteUser, |
| | | fetchExportUserReport, |
| | | fetchGetDeptTree, |
| | | fetchGetRoleOptions, |
| | | fetchGetUserDetail, |
| | |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailUserData = ref({}) |
| | | const selectedRows = ref([]) |
| | | const roleOptions = ref([]) |
| | | const deptTreeOptions = ref([]) |
| | | const exportLoading = ref(false) |
| | | const RESET_PASSWORD = '123456' |
| | | const { hasAuth } = useAuth() |
| | | const userStore = useUserStore() |
| | | |
| | | const fetchUserPage = (params = {}) => { |
| | | return request.post({ |
| | |
| | | apiParams: buildUserPageQueryParams(searchForm.value), |
| | | paginationKey: getUserPaginationKey(), |
| | | columnsFactory: () => [ |
| | | { |
| | | type: 'selection', |
| | | width: 52, |
| | | fixed: 'left', |
| | | align: 'center' |
| | | }, |
| | | { |
| | | prop: 'username', |
| | | label: '用户名', |
| | |
| | | getData() |
| | | } |
| | | |
| | | const handleSelectionChange = (rows) => { |
| | | selectedRows.value = Array.isArray(rows) ? rows : [] |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | Object.assign(searchForm.value, createUserSearchState()) |
| | | resetSearchParams() |
| | |
| | | |
| | | const handleDelete = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要删除用户「${row.username || row.nickname || row.id}」吗?`, '删除确认', { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除用户「${row.username || row.nickname || row.id}」吗?`, |
| | | '删除确认', |
| | | { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | } |
| | | ) |
| | | await fetchDeleteUser(row.id) |
| | | ElMessage.success('删除成功') |
| | | await refreshRemove() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error(error?.message || '删除失败') |
| | | } |
| | | } |
| | | } |
| | | |
| | | const handleBatchDelete = async () => { |
| | | if (!selectedRows.value.length) return |
| | | const ids = selectedRows.value |
| | | .map((item) => item?.id) |
| | | .filter((id) => id !== void 0 && id !== null) |
| | | |
| | | if (!ids.length) return |
| | | |
| | | try { |
| | | await ElMessageBox.confirm(`确定要批量删除选中的 ${ids.length} 个用户吗?`, '批量删除确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | await fetchDeleteUser(ids.join(',')) |
| | | ElMessage.success('批量删除成功') |
| | | selectedRows.value = [] |
| | | await refreshRemove() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error(error?.message || '批量删除失败') |
| | | } |
| | | } |
| | | } |
| | |
| | | row._statusLoading = false |
| | | } |
| | | } |
| | | |
| | | const handleExport = async () => { |
| | | exportLoading.value = true |
| | | try { |
| | | const response = await guardRequestWithMessage( |
| | | fetchExportUserReport(buildUserSearchParams(searchForm.value), { |
| | | headers: { |
| | | Authorization: userStore.accessToken || '' |
| | | } |
| | | }), |
| | | null, |
| | | { |
| | | timeoutMessage: '用户导出超时,已停止等待' |
| | | } |
| | | ) |
| | | if (!response) { |
| | | return |
| | | } |
| | | if (!response.ok) { |
| | | throw new Error(`导出失败,状态码:${response.status}`) |
| | | } |
| | | const blob = await response.blob() |
| | | const downloadUrl = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = downloadUrl |
| | | link.download = 'user.xlsx' |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | link.remove() |
| | | window.URL.revokeObjectURL(downloadUrl) |
| | | ElMessage.success('导出成功') |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '导出失败') |
| | | } finally { |
| | | exportLoading.value = false |
| | | } |
| | | } |
| | | </script> |
| | |
| | | <ElDescriptionsItem label="邮箱">{{ displayData.email || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="真实姓名">{{ displayData.realName || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="身份证号">{{ displayData.idCard || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="出生日期">{{ displayData.birthday || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="工号">{{ displayData.code || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="性别">{{ sexLabel }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="个人简介">{{ |
| | | displayData.introduction || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="创建时间">{{ |
| | | displayData.createTimeText || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="更新时间">{{ |
| | | displayData.updateTimeText || '--' |
| | | }}</ElDescriptionsItem> |
| | | <ElDescriptionsItem label="备注">{{ displayData.memo || '--' }}</ElDescriptionsItem> |
| | | </ElDescriptions> |
| | | </ElSkeleton> |
| | |
| | | const emit = defineEmits(['update:visible']) |
| | | |
| | | const displayData = computed(() => normalizeUserListRow(props.userData)) |
| | | const statusLabel = computed(() => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text) |
| | | const statusLabel = computed( |
| | | () => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text |
| | | ) |
| | | const sexLabel = computed(() => { |
| | | switch (displayData.value.sex) { |
| | | case 1: |
| | |
| | | <ElDialog |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | @update:model-value="handleCancel" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | @update:model-value="handleVisibleUpdate" |
| | | width="960px" |
| | | align-center |
| | | class="user-dialog" |
| | |
| | | |
| | | <script setup> |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import { buildUserDialogModel, createUserFormState } from '../userPage.helpers' |
| | | |
| | | const props = defineProps({ |
| | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const form = reactive(createUserFormState()) |
| | | const initialSnapshot = ref(createComparableSnapshot()) |
| | | |
| | | const isEdit = computed(() => props.type === 'edit') |
| | | const dialogTitle = computed(() => (isEdit.value ? '编辑用户' : '新增用户')) |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: '出生日期', |
| | | key: 'birthday', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入出生日期', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '个人简介', |
| | | key: 'introduction', |
| | | type: 'input', |
| | | props: { |
| | | type: 'textarea', |
| | | rows: 3, |
| | | placeholder: '请输入个人简介', |
| | | clearable: true |
| | | }, |
| | | span: 24 |
| | | }, |
| | | { |
| | | label: '状态', |
| | | key: 'status', |
| | | type: 'select', |
| | |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form, createUserFormState()) |
| | | initialSnapshot.value = createComparableSnapshot() |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | const loadFormData = () => { |
| | | Object.assign(form, buildUserDialogModel(props.userData)) |
| | | const nextForm = buildUserDialogModel(props.userData) |
| | | Object.assign(form, nextForm) |
| | | initialSnapshot.value = createComparableSnapshot(nextForm) |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | |
| | | } |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | const closeDialog = () => { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | const handleCancel = async () => { |
| | | if (!(await confirmDiscardIfDirty())) { |
| | | return |
| | | } |
| | | closeDialog() |
| | | } |
| | | |
| | | const handleVisibleUpdate = (nextVisible) => { |
| | | if (!nextVisible) { |
| | | handleCancel() |
| | | return |
| | | } |
| | | emit('update:visible', true) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | |
| | | } |
| | | } |
| | | ) |
| | | |
| | | function createComparableSnapshot(source = form) { |
| | | return JSON.stringify({ |
| | | ...createUserFormState(), |
| | | ...source, |
| | | roleIds: Array.isArray(source?.roleIds) ? [...source.roleIds] : [], |
| | | userRoleIds: Array.isArray(source?.userRoleIds) ? [...source.userRoleIds] : [] |
| | | }) |
| | | } |
| | | |
| | | function isDirty() { |
| | | return createComparableSnapshot() !== initialSnapshot.value |
| | | } |
| | | |
| | | async function confirmDiscardIfDirty() { |
| | | if (!isDirty()) { |
| | | return true |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', { |
| | | confirmButtonText: '放弃修改', |
| | | cancelButtonText: '继续编辑', |
| | | type: 'warning' |
| | | }) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | } |
| | | </script> |
| | |
| | | } |
| | | }, |
| | | { |
| | | label: '备注', |
| | | key: 'memo', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入备注', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '关键字', |
| | | key: 'condition', |
| | | type: 'input', |
| | |
| | | sex: void 0, |
| | | realName: '', |
| | | idCard: '', |
| | | memo: '', |
| | | condition: '' |
| | | } |
| | | } |
| | |
| | | email: '', |
| | | realName: '', |
| | | idCard: '', |
| | | birthday: '', |
| | | introduction: '', |
| | | memo: '', |
| | | status: 1 |
| | | } |
| | |
| | | sex: params.sex, |
| | | realName: params.realName, |
| | | idCard: params.idCard, |
| | | memo: params.memo, |
| | | condition: params.condition |
| | | } |
| | | |
| | |
| | | email: record.email || '', |
| | | realName: record.realName || '', |
| | | idCard: record.idCard || '', |
| | | birthday: record.birthday || '', |
| | | introduction: record.introduction || '', |
| | | memo: record.memo || '', |
| | | status: record.status !== undefined && record.status !== null ? record.status : 1 |
| | | } |
| | |
| | | email: form.email || '', |
| | | realName: form.realName || '', |
| | | idCard: form.idCard || '', |
| | | birthday: form.birthday || '', |
| | | introduction: form.introduction || '', |
| | | memo: form.memo || '', |
| | | status: form.status !== undefined && form.status !== null ? form.status : 1 |
| | | } |
| | |
| | | return [] |
| | | } |
| | | |
| | | return tree |
| | | .map((node) => normalizeDeptTreeNode(node)) |
| | | .filter(Boolean) |
| | | return tree.map((node) => normalizeDeptTreeNode(node)).filter(Boolean) |
| | | } |
| | | |
| | | export function normalizeRoleOptions(roles = []) { |
| | |
| | | : [] |
| | | |
| | | return Array.from( |
| | | new Set( |
| | | directRoleIds |
| | | .map((item) => normalizeRoleId(item)) |
| | | .filter((item) => item !== void 0) |
| | | ) |
| | | new Set(directRoleIds.map((item) => normalizeRoleId(item)).filter((item) => item !== void 0)) |
| | | ) |
| | | } |
| | | |