From 6877c9caa25162e570a3e2a99a5b2ce3ef88368b Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 13 四月 2026 13:48:37 +0800
Subject: [PATCH] #页面优化
---
rsf-design/src/api/system-manage.js | 71 ++
rsf-design/src/views/system/dict-type/dictDataPage.helpers.js | 147 +++++
rsf-design/src/views/system/user/modules/user-dialog.vue | 78 ++
rsf-design/src/views/system/dict-type/dictTypePage.helpers.js | 14
rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue | 333 +++++++++++
rsf-design/src/views/system/dict-type/dictDataTable.columns.js | 94 +++
rsf-design/src/views/system/dict-type/dictTypeTable.columns.js | 48 +
rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue | 245 ++++++++
rsf-design/src/views/system/role/roleTable.columns.js | 10
rsf-design/src/views/system/role/rolePage.helpers.js | 33
rsf-design/src/views/system/user/modules/user-detail-drawer.vue | 16
rsf-design/src/views/system/role/modules/role-permission-dialog.vue | 86 ++
rsf-design/src/views/system/dict-type/index.vue | 110 +++
rsf-design/src/views/system/menu/modules/menu-dialog.vue | 50 +
rsf-design/src/views/system/menu/index.vue | 16
rsf-design/src/views/system/menu/menuPage.helpers.js | 28
rsf-design/src/views/system/user/index.vue | 104 +++
rsf-design/src/views/system/user/userPage.helpers.js | 18
rsf-design/src/views/system/user/modules/user-search.vue | 9
rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue | 63 ++
rsf-design/src/views/system/menu/menuTable.columns.js | 5
rsf-design/src/views/system/role/modules/role-edit-dialog.vue | 83 ++
rsf-design/src/api/auth.js | 2
23 files changed, 1,565 insertions(+), 98 deletions(-)
diff --git a/rsf-design/src/api/auth.js b/rsf-design/src/api/auth.js
index 0d070a1..36b0ab9 100644
--- a/rsf-design/src/api/auth.js
+++ b/rsf-design/src/api/auth.js
@@ -114,7 +114,7 @@
}
function fetchGetMenuList() {
return request.get({
- url: '/auth/menu/v2'
+ url: '/auth/menu'
})
}
export {
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
index 2e62426..44abc64 100644
--- a/rsf-design/src/api/system-manage.js
+++ b/rsf-design/src/api/system-manage.js
@@ -9,7 +9,14 @@
...(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 } : {})
}
}
@@ -92,7 +99,8 @@
...(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 } : {})
}
}
@@ -108,7 +116,8 @@
...(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 } : {})
}
}
@@ -208,6 +217,17 @@
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) })
}
@@ -239,16 +259,54 @@
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 = {}) {
@@ -880,6 +938,7 @@
fetchResetUserPassword,
fetchUpdateUserStatus,
fetchGetUserDetail,
+ fetchExportUserReport,
fetchGetRoleList,
fetchOperationRecordPage,
fetchGetOperationRecordDetail,
@@ -901,9 +960,15 @@
fetchDeleteSerialRule,
fetchDictTypePage,
fetchGetDictTypeDetail,
+ fetchGetDictDataDetail,
fetchSaveDictType,
+ fetchSaveDictData,
fetchUpdateDictType,
+ fetchUpdateDictData,
fetchDeleteDictType,
+ fetchDeleteDictData,
+ fetchExportDictTypeReport,
+ fetchExportDictDataReport,
fetchDictDataPage,
fetchWaveRulePage,
fetchGetWaveRuleDetail,
diff --git a/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js b/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js
new file mode 100644
index 0000000..9bbd4cd
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js
@@ -0,0 +1,147 @@
+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)
+ }
+}
diff --git a/rsf-design/src/views/system/dict-type/dictDataTable.columns.js b/rsf-design/src/views/system/dict-type/dictDataTable.columns.js
new file mode 100644
index 0000000..63add5b
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictDataTable.columns.js
@@ -0,0 +1,94 @@
+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)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js b/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
index 752260e..f357a74 100644
--- a/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
+++ b/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
@@ -1,11 +1,18 @@
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
}
}
@@ -38,6 +45,11 @@
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) }
: {})
diff --git a/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js b/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
index a0c5190..4211382 100644
--- a/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
+++ b/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
@@ -3,7 +3,15 @@
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',
@@ -29,7 +37,11 @@
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',
@@ -46,19 +58,35 @@
{
prop: 'operation',
label: t('table.operation'),
- width: handleDelete ? 160 : 120,
+ width: handleDelete ? (hasStandaloneView ? 200 : 240) : hasStandaloneView ? 160 : 200,
align: 'center',
formatter: (row) => {
- const buttons = [
- h(ArtButtonTable, {
- type: 'view',
- onClick: () => handleView(row)
- }),
+ 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(
@@ -69,7 +97,7 @@
)
}
- return h('div', { class: 'flex justify-center' }, buttons)
+ return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
}
}
]
diff --git a/rsf-design/src/views/system/dict-type/index.vue b/rsf-design/src/views/system/dict-type/index.vue
index cdba2f6..8f0dc32 100644
--- a/rsf-design/src/views/system/dict-type/index.vue
+++ b/rsf-design/src/views/system/dict-type/index.vue
@@ -12,7 +12,9 @@
<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"
@@ -21,6 +23,15 @@
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>
@@ -47,6 +58,11 @@
:loading="detailLoading"
:detail-data="detailData"
/>
+
+ <DictDataPanel
+ v-model:visible="dictDataPanelVisible"
+ :dict-type-data="currentManagedDictTypeData"
+ />
</ElCard>
</div>
</template>
@@ -54,16 +70,20 @@
<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'
@@ -81,10 +101,14 @@
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(() => [
@@ -116,6 +140,24 @@
}
},
{
+ 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',
@@ -125,6 +167,26 @@
{ 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'
}
}
])
@@ -153,6 +215,11 @@
}
}
+ function openDictDataPanel(row) {
+ currentManagedDictTypeData.value = buildDictTypeDialogModel(row)
+ dictDataPanelVisible.value = true
+ }
+
const {
columns,
columnChecks,
@@ -178,6 +245,9 @@
handleView: openDetail,
handleEdit: openEditDialog,
handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ handleManageData: hasAuth('system:dictData:list')
+ ? (row) => openDictDataPanel(row)
+ : null,
t
})
},
@@ -216,6 +286,44 @@
})
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()
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue b/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue
new file mode 100644
index 0000000..fbf0aea
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue
@@ -0,0 +1,245 @@
+<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>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue b/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue
new file mode 100644
index 0000000..bdcd3ae
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue
@@ -0,0 +1,333 @@
+<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>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue b/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
index 178ddc0..04e46cb 100644
--- a/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
+++ b/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
@@ -2,9 +2,11 @@
<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
@@ -29,6 +31,7 @@
</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'
@@ -41,15 +44,20 @@
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' }]
}))
@@ -110,11 +118,14 @@
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() {
@@ -127,8 +138,23 @@
}
}
- 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() {
@@ -155,4 +181,31 @@
},
{ 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>
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index ea98983..2a201d2 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/rsf-design/src/views/system/menu/index.vue
@@ -16,9 +16,15 @@
@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>
@@ -153,7 +159,7 @@
function handleAddMenu() {
dialogType.value = 'menu'
editData.value = null
- lockMenuType.value = true
+ lockMenuType.value = false
dialogVisible.value = true
}
@@ -172,14 +178,14 @@
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
}
diff --git a/rsf-design/src/views/system/menu/menuPage.helpers.js b/rsf-design/src/views/system/menu/menuPage.helpers.js
index 03d60e3..e69fb5a 100644
--- a/rsf-design/src/views/system/menu/menuPage.helpers.js
+++ b/rsf-design/src/views/system/menu/menuPage.helpers.js
@@ -28,7 +28,13 @@
}
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 = {}) {
@@ -69,7 +75,11 @@
}))
}
-export function buildMenuTreeOptions(tree = [], titleFormatter = defaultMenuTitleFormatter, t = $t) {
+export function buildMenuTreeOptions(
+ tree = [],
+ titleFormatter = defaultMenuTitleFormatter,
+ t = $t
+) {
return [
{
label: t('table.topLevelMenu'),
@@ -137,10 +147,18 @@
})
}
-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()
diff --git a/rsf-design/src/views/system/menu/menuTable.columns.js b/rsf-design/src/views/system/menu/menuTable.columns.js
index e038694..3d2e11b 100644
--- a/rsf-design/src/views/system/menu/menuTable.columns.js
+++ b/rsf-design/src/views/system/menu/menuTable.columns.js
@@ -81,10 +81,7 @@
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 || ''
}
},
{
diff --git a/rsf-design/src/views/system/menu/modules/menu-dialog.vue b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
index eb0e103..0981837 100644
--- a/rsf-design/src/views/system/menu/modules/menu-dialog.vue
+++ b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
@@ -2,6 +2,7 @@
<ElDialog
:title="dialogTitle"
:model-value="visible"
+ :close-on-click-modal="false"
@update:model-value="handleCancel"
width="760px"
align-center
@@ -70,20 +71,49 @@
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'
+ }
+ ]
: []
}))
@@ -109,12 +139,18 @@
}
},
{
- 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
}
}
diff --git a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
index fbec2b9..e4fa75a 100644
--- a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
@@ -2,9 +2,11 @@
<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
@@ -30,7 +32,12 @@
<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({
@@ -42,6 +49,7 @@
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')
@@ -50,7 +58,9 @@
)
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 = {}) {
@@ -67,7 +77,14 @@
}
}
- function createSelectFormItem(label, key, placeholder, options, extraProps = {}, extraConfig = {}) {
+ function createSelectFormItem(
+ label,
+ key,
+ placeholder,
+ options,
+ extraProps = {},
+ extraConfig = {}
+ ) {
return {
label,
key,
@@ -110,11 +127,14 @@
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 () => {
@@ -127,8 +147,23 @@
}
}
- 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 = () => {
@@ -157,4 +192,40 @@
},
{ 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>
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
index 8d8909a..25f22dc 100644
--- a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -24,10 +24,23 @@
<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">
@@ -38,16 +51,19 @@
@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)"
@@ -73,10 +89,13 @@
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'
@@ -92,21 +111,28 @@
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 {
@@ -115,7 +141,9 @@
treeData: [],
checkedKeys: [],
halfCheckedKeys: [],
- condition: ''
+ condition: '',
+ expandAll: true,
+ treeVersion: 0
}
}
@@ -135,13 +163,17 @@
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) {
@@ -155,9 +187,12 @@
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(() => {
@@ -207,6 +242,16 @@
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 {
@@ -241,7 +286,7 @@
}
if (activeScopeType.value === 'menu') {
const resolvedTitle = resolveBackendMenuTitle(rawLabel, data?.component || '')
- return resolvedTitle ? formatMenuTitle(resolvedTitle) : ''
+ return resolvedTitle ? formatMenuTitle(resolvedTitle) : rawLabel
}
return rawLabel
}
@@ -292,12 +337,9 @@
}
)
- watch(
- activeScopeType,
- async (scopeType) => {
- if (props.visible && scopeType) {
- await ensureScopeLoaded(scopeType)
- }
+ watch(activeScopeType, async (scopeType) => {
+ if (props.visible && scopeType) {
+ await ensureScopeLoaded(scopeType)
}
- )
+ })
</script>
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
index 791f914..7e41492 100644
--- a/rsf-design/src/views/system/role/rolePage.helpers.js
+++ b/rsf-design/src/views/system/role/rolePage.helpers.js
@@ -5,13 +5,16 @@
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
}
}
@@ -46,7 +49,8 @@
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)))
@@ -260,7 +264,7 @@
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 || '',
@@ -275,9 +279,7 @@
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 || '',
@@ -292,7 +294,7 @@
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 || '',
@@ -326,6 +328,23 @@
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
diff --git a/rsf-design/src/views/system/role/roleTable.columns.js b/rsf-design/src/views/system/role/roleTable.columns.js
index 4f436f4..692abd1 100644
--- a/rsf-design/src/views/system/role/roleTable.columns.js
+++ b/rsf-design/src/views/system/role/roleTable.columns.js
@@ -63,7 +63,11 @@
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 || '-'
+ )
}
}
}
@@ -74,7 +78,9 @@
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 || '-'
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index 8800cf1..53fcf15 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/rsf-design/src/views/system/user/index.vue
@@ -14,6 +14,18 @@
<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>
@@ -23,6 +35,7 @@
:data="data"
:columns="columns"
:pagination="pagination"
+ @selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
/>
@@ -48,8 +61,10 @@
<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,
@@ -89,10 +104,13 @@
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({
@@ -122,6 +140,12 @@
apiParams: buildUserPageQueryParams(searchForm.value),
paginationKey: getUserPaginationKey(),
columnsFactory: () => [
+ {
+ type: 'selection',
+ width: 52,
+ fixed: 'left',
+ align: 'center'
+ },
{
prop: 'username',
label: '鐢ㄦ埛鍚�',
@@ -277,6 +301,10 @@
getData()
}
+ const handleSelectionChange = (rows) => {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
const handleReset = () => {
Object.assign(searchForm.value, createUserSearchState())
resetSearchParams()
@@ -342,17 +370,46 @@
const handleDelete = async (row) => {
try {
- await ElMessageBox.confirm(`纭畾瑕佸垹闄ょ敤鎴枫��${row.username || row.nickname || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
- confirmButtonText: '纭畾',
- cancelButtonText: '鍙栨秷',
- type: 'warning'
- })
+ 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 || '鎵归噺鍒犻櫎澶辫触')
}
}
}
@@ -404,4 +461,41 @@
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>
diff --git a/rsf-design/src/views/system/user/modules/user-detail-drawer.vue b/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
index 9bf4210..8fccc71 100644
--- a/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
+++ b/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
@@ -16,10 +16,18 @@
<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>
@@ -38,7 +46,9 @@
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:
diff --git a/rsf-design/src/views/system/user/modules/user-dialog.vue b/rsf-design/src/views/system/user/modules/user-dialog.vue
index 72725a3..85a50be 100644
--- a/rsf-design/src/views/system/user/modules/user-dialog.vue
+++ b/rsf-design/src/views/system/user/modules/user-dialog.vue
@@ -2,7 +2,9 @@
<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"
@@ -31,6 +33,7 @@
<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({
@@ -44,6 +47,7 @@
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 ? '缂栬緫鐢ㄦ埛' : '鏂板鐢ㄦ埛'))
@@ -214,6 +218,27 @@
}
},
{
+ 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',
@@ -242,11 +267,14 @@
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 () => {
@@ -259,8 +287,23 @@
}
}
- 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 = () => {
@@ -298,4 +341,33 @@
}
}
)
+
+ 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>
diff --git a/rsf-design/src/views/system/user/modules/user-search.vue b/rsf-design/src/views/system/user/modules/user-search.vue
index 849a4bc..0a56626 100644
--- a/rsf-design/src/views/system/user/modules/user-search.vue
+++ b/rsf-design/src/views/system/user/modules/user-search.vue
@@ -133,6 +133,15 @@
}
},
{
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ },
+ {
label: '鍏抽敭瀛�',
key: 'condition',
type: 'input',
diff --git a/rsf-design/src/views/system/user/userPage.helpers.js b/rsf-design/src/views/system/user/userPage.helpers.js
index 8189f25..91cdbda 100644
--- a/rsf-design/src/views/system/user/userPage.helpers.js
+++ b/rsf-design/src/views/system/user/userPage.helpers.js
@@ -11,6 +11,7 @@
sex: void 0,
realName: '',
idCard: '',
+ memo: '',
condition: ''
}
}
@@ -31,6 +32,8 @@
email: '',
realName: '',
idCard: '',
+ birthday: '',
+ introduction: '',
memo: '',
status: 1
}
@@ -49,6 +52,7 @@
sex: params.sex,
realName: params.realName,
idCard: params.idCard,
+ memo: params.memo,
condition: params.condition
}
@@ -94,6 +98,8 @@
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
}
@@ -135,6 +141,8 @@
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
}
@@ -171,9 +179,7 @@
return []
}
- return tree
- .map((node) => normalizeDeptTreeNode(node))
- .filter(Boolean)
+ return tree.map((node) => normalizeDeptTreeNode(node)).filter(Boolean)
}
export function normalizeRoleOptions(roles = []) {
@@ -254,11 +260,7 @@
: []
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))
)
}
--
Gitblit v1.9.1