From e9283ffe6822b12ec5dd2ccf4dc13a369b227a61 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:32:06 +0800
Subject: [PATCH] chore: sync rsf-design from isolated worktree
---
rsf-design/src/directives/core/roles.js | 21
rsf-design/src/config/index.js | 2
rsf-design/src/views/system/user/modules/user-dialog.vue | 355 +++++-
rsf-design/src/views/auth/login/index.vue | 118 --
rsf-design/src/directives/core/auth.js | 15
rsf-design/src/views/system/role/modules/role-search.vue | 97 +-
rsf-design/src/views/dashboard/console/index.vue | 271 +++++
rsf-design/build/manualChunks.js | 49 +
rsf-design/src/locales/langs/en.json | 2
rsf-design/src/views/system/role/rolePage.helpers.js | 320 ++++++
rsf-design/src/locales/langs/zh.json | 2
rsf-design/src/views/system/user/modules/user-detail-drawer.vue | 56 +
rsf-design/src/router/core/MenuProcessor.js | 5
rsf-design/tests/system-role-print-export-page.test.mjs | 97 ++
rsf-design/src/router/guards/beforeEach.js | 4
rsf-design/src/views/system/user/index.vue | 444 ++++++--
rsf-design/tests/auth-contract.test.mjs | 123 ++
rsf-design/src/hooks/core/useAuth.js | 103 ++
rsf-design/src/views/system/role/index.vue | 428 +++++++--
rsf-design/tests/list-export-print-contract.test.mjs | 103 ++
rsf-design/src/api/auth.js | 116 ++
rsf-design/src/store/modules/user.js | 8
22 files changed, 2,204 insertions(+), 535 deletions(-)
diff --git a/rsf-design/build/manualChunks.js b/rsf-design/build/manualChunks.js
new file mode 100644
index 0000000..f803252
--- /dev/null
+++ b/rsf-design/build/manualChunks.js
@@ -0,0 +1,49 @@
+const CHUNK_GROUPS = [
+ {
+ name: 'vendor-echarts',
+ packages: ['echarts/']
+ },
+ {
+ name: 'vendor-editor',
+ packages: ['@wangeditor/', 'highlight.js/']
+ },
+ {
+ name: 'vendor-xlsx',
+ packages: ['xlsx/']
+ },
+ {
+ name: 'vendor-media',
+ packages: ['xgplayer/']
+ },
+ {
+ name: 'vendor-element-plus',
+ packages: ['element-plus/', '@element-plus/']
+ },
+ {
+ name: 'vendor-vue',
+ packages: ['vue-router/', 'pinia/', '@vueuse/']
+ },
+ {
+ name: 'vendor-utils',
+ packages: ['@iconify/', 'file-saver/', 'axios/']
+ }
+]
+
+function createManualChunks(id) {
+ if (!id || !id.includes('/node_modules/')) {
+ return void 0
+ }
+
+ const normalizedId = id.replace(/\\/g, '/')
+ const packagePath = normalizedId.split('/node_modules/').pop() || ''
+
+ for (const group of CHUNK_GROUPS) {
+ if (group.packages.some((pkg) => packagePath.startsWith(pkg))) {
+ return group.name
+ }
+ }
+
+ return void 0
+}
+
+export { createManualChunks }
diff --git a/rsf-design/src/api/auth.js b/rsf-design/src/api/auth.js
index b7c9fbb..c420267 100644
--- a/rsf-design/src/api/auth.js
+++ b/rsf-design/src/api/auth.js
@@ -1,19 +1,111 @@
import request from '@/utils/http'
+
+function buildLoginPayload({ username, password }) {
+ return { username, password }
+}
+
+function normalizeLoginParams(params) {
+ return {
+ username: params?.username || params?.userName || '',
+ password: params?.password
+ }
+}
+
+function normalizeLoginResponse(payload) {
+ const data = payload?.data || payload || {}
+ return {
+ accessToken: data.accessToken || '',
+ refreshToken: data.refreshToken || '',
+ user: data.user || {}
+ }
+}
+
+function normalizeUserInfo(data) {
+ const normalizedRoles = normalizeRoleCodes(data?.roles)
+ const normalizedButtons = normalizeButtonMarks(data)
+ return {
+ ...data,
+ roles: normalizedRoles,
+ buttons: normalizedButtons
+ }
+}
+
+function normalizeRoleCodes(roles) {
+ if (!Array.isArray(roles)) {
+ return []
+ }
+
+ return Array.from(
+ new Set(
+ roles
+ .map((item) => {
+ if (typeof item === 'string') {
+ return item.trim()
+ }
+ if (item && typeof item === 'object') {
+ return item.code || item.name || ''
+ }
+ return ''
+ })
+ .filter(Boolean)
+ )
+ )
+}
+
+function normalizeButtonMarks(data) {
+ const directButtons = Array.isArray(data?.buttons) ? data.buttons : []
+ const authorityButtons = Array.isArray(data?.authorities)
+ ? data.authorities.map((item) => item?.authority || item?.authMark || '')
+ : []
+
+ return Array.from(
+ new Set(
+ [...directButtons, ...authorityButtons]
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
+ .filter(Boolean)
+ )
+ )
+}
+
function fetchLogin(params) {
- return request.post({
- url: '/api/auth/login',
- params
- // showSuccessMessage: true // 鏄剧ず鎴愬姛娑堟伅
- // showErrorMessage: false // 涓嶆樉绀洪敊璇秷鎭�
- })
+ return request
+ .post({
+ url: '/login',
+ params: buildLoginPayload(normalizeLoginParams(params))
+ // showSuccessMessage: true // 鏄剧ず鎴愬姛娑堟伅
+ // showErrorMessage: false // 涓嶆樉绀洪敊璇秷鎭�
+ })
+ .then((response) => {
+ const normalized = normalizeLoginResponse(response)
+ return {
+ token: normalized.accessToken,
+ accessToken: normalized.accessToken,
+ refreshToken: normalized.refreshToken,
+ user: normalized.user
+ }
+ })
}
function fetchGetUserInfo() {
+ return request
+ .get({
+ url: '/auth/user'
+ // 鑷畾涔夎姹傚ご
+ // headers: {
+ // 'X-Custom-Header': 'your-custom-value'
+ // }
+ })
+ .then((response) => normalizeUserInfo(response))
+}
+function fetchGetMenuList() {
return request.get({
- url: '/api/user/info'
- // 鑷畾涔夎姹傚ご
- // headers: {
- // 'X-Custom-Header': 'your-custom-value'
- // }
+ url: '/auth/menu'
})
}
-export { fetchGetUserInfo, fetchLogin }
+export {
+ buildLoginPayload,
+ fetchGetMenuList,
+ fetchGetUserInfo,
+ fetchLogin,
+ normalizeLoginResponse,
+ normalizeUserInfo
+}
diff --git a/rsf-design/src/config/index.js b/rsf-design/src/config/index.js
index 5880ec6..71de6ff 100644
--- a/rsf-design/src/config/index.js
+++ b/rsf-design/src/config/index.js
@@ -5,7 +5,7 @@
const appConfig = {
// 绯荤粺淇℃伅
systemInfo: {
- name: 'Art Design Pro'
+ name: 'RSF Design'
// 绯荤粺鍚嶇О
},
// 绯荤粺涓婚
diff --git a/rsf-design/src/directives/core/auth.js b/rsf-design/src/directives/core/auth.js
index 266328b..21f96eb 100644
--- a/rsf-design/src/directives/core/auth.js
+++ b/rsf-design/src/directives/core/auth.js
@@ -1,7 +1,18 @@
import { router } from '@/router'
+import { useUserStore } from '@/store/modules/user'
+import { useAppMode } from '@/hooks/core/useAppMode'
+import { extractRouteAuthMarks, extractUserButtons, hasAuthPermission } from '@/hooks/core/useAuth'
+
function checkAuthPermission(el, binding) {
- const authList = router.currentRoute.value.meta.authList || []
- const hasPermission = authList.some((item) => item.authMark === binding.value)
+ const authList = extractRouteAuthMarks(router.currentRoute.value.meta.authList)
+ const buttons = extractUserButtons(useUserStore().getUserInfo)
+ const { isBackendMode } = useAppMode()
+ const hasPermission = hasAuthPermission(binding.value, {
+ authList,
+ buttons,
+ isBackendMode: isBackendMode.value,
+ routePath: router.currentRoute.value.path
+ })
if (!hasPermission) {
removeElement(el)
}
diff --git a/rsf-design/src/directives/core/roles.js b/rsf-design/src/directives/core/roles.js
index 2fb4155..bf0c186 100644
--- a/rsf-design/src/directives/core/roles.js
+++ b/rsf-design/src/directives/core/roles.js
@@ -1,7 +1,26 @@
import { useUserStore } from '@/store/modules/user'
+
+function extractRoleCodes(roles) {
+ if (!Array.isArray(roles)) {
+ return []
+ }
+
+ return roles
+ .map((item) => {
+ if (typeof item === 'string') {
+ return item
+ }
+ if (item && typeof item === 'object') {
+ return item.code || item.name || ''
+ }
+ return ''
+ })
+ .filter(Boolean)
+}
+
function checkRolePermission(el, binding) {
const userStore = useUserStore()
- const userRoles = userStore.getUserInfo.roles
+ const userRoles = extractRoleCodes(userStore.getUserInfo.roles)
if (!userRoles?.length) {
removeElement(el)
return
diff --git a/rsf-design/src/hooks/core/useAuth.js b/rsf-design/src/hooks/core/useAuth.js
index 5a4949c..b222810 100644
--- a/rsf-design/src/hooks/core/useAuth.js
+++ b/rsf-design/src/hooks/core/useAuth.js
@@ -2,21 +2,106 @@
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { useAppMode } from '@/hooks/core/useAppMode'
-const userStore = useUserStore()
+
+function extractRouteAuthMarks(authList) {
+ if (!Array.isArray(authList)) {
+ return []
+ }
+
+ return authList
+ .map((item) => {
+ if (typeof item === 'string') {
+ return item
+ }
+ if (item && typeof item === 'object') {
+ return item.authMark || ''
+ }
+ return ''
+ })
+ .filter(Boolean)
+}
+
+function extractUserButtons(info) {
+ return Array.isArray(info?.buttons) ? info.buttons : []
+}
+
+const BUTTON_ACTION_MAP = {
+ query: 'list',
+ add: 'save',
+ edit: 'update',
+ delete: 'remove'
+}
+
+function resolveRouteResourceKey(routePath) {
+ const pathSegments = String(routePath || '')
+ .split('/')
+ .filter(Boolean)
+
+ const rawSegment = pathSegments[pathSegments.length - 1]
+ if (!rawSegment) {
+ return ''
+ }
+
+ return rawSegment.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
+}
+
+function matchesBackendButton(requiredAuth, buttons, routePath) {
+ if (buttons.includes(requiredAuth)) {
+ return true
+ }
+
+ const action = BUTTON_ACTION_MAP[requiredAuth]
+ const resourceKey = resolveRouteResourceKey(routePath)
+ if (!action || !resourceKey) {
+ return false
+ }
+
+ return buttons.some(
+ (item) =>
+ typeof item === 'string' && item.includes(`:${resourceKey}:`) && item.endsWith(`:${action}`)
+ )
+}
+
+function hasAuthPermission(
+ requiredAuth,
+ { authList = [], buttons = [], isBackendMode = false, routePath = '' } = {}
+) {
+ const requiredList = Array.isArray(requiredAuth) ? requiredAuth : [requiredAuth]
+
+ if (!requiredList.length) {
+ return true
+ }
+
+ if (isBackendMode) {
+ return requiredList.some((item) => matchesBackendButton(item, buttons, routePath))
+ }
+
+ return requiredList.some((item) => authList.includes(item))
+}
+
const useAuth = () => {
+ const userStore = useUserStore()
const route = useRoute()
- const { isFrontendMode } = useAppMode()
+ const { isBackendMode } = useAppMode()
const { info } = storeToRefs(userStore)
- const frontendAuthList = info.value?.buttons ?? []
- const backendAuthList = Array.isArray(route.meta.authList) ? route.meta.authList : []
+ const authList = computed(() => extractRouteAuthMarks(route.meta.authList))
+ const buttons = computed(() => extractUserButtons(info.value))
const hasAuth = (auth) => {
- if (isFrontendMode.value) {
- return frontendAuthList.includes(auth)
- }
- return backendAuthList.some((item) => item?.authMark === auth)
+ return hasAuthPermission(auth, {
+ authList: authList.value,
+ buttons: buttons.value,
+ isBackendMode: isBackendMode.value,
+ routePath: route.path
+ })
}
return {
hasAuth
}
}
-export { useAuth }
+export {
+ extractRouteAuthMarks,
+ extractUserButtons,
+ hasAuthPermission,
+ resolveRouteResourceKey,
+ useAuth
+}
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index e9ee97d..3b7f747 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -257,9 +257,11 @@
"notFound": "404",
"serverError": "500"
},
+ "userLogin": "Login Logs",
"system": {
"title": "System Settings",
"user": "User Manage",
+ "userLogin": "Login Logs",
"role": "Role Manage",
"userCenter": "User Center",
"menu": "Menu Manage"
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index c72e9ea..fe64198 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -257,9 +257,11 @@
"notFound": "404",
"serverError": "500"
},
+ "userLogin": "鐧诲綍鏃ュ織",
"system": {
"title": "绯荤粺绠$悊",
"user": "鐢ㄦ埛绠$悊",
+ "userLogin": "鐧诲綍鏃ュ織",
"role": "瑙掕壊绠$悊",
"userCenter": "涓汉涓績",
"menu": "鑿滃崟绠$悊"
diff --git a/rsf-design/src/router/core/MenuProcessor.js b/rsf-design/src/router/core/MenuProcessor.js
index 1a32f2f..f031609 100644
--- a/rsf-design/src/router/core/MenuProcessor.js
+++ b/rsf-design/src/router/core/MenuProcessor.js
@@ -1,8 +1,9 @@
import { useUserStore } from '@/store/modules/user'
import { useAppMode } from '@/hooks/core/useAppMode'
-import { fetchGetMenuList } from '@/api/system-manage'
+import { fetchGetMenuList } from '@/api/auth'
import { asyncRoutes } from '../routes/asyncRoutes'
import { RoutesAlias } from '../routesAlias'
+import { adaptBackendMenuTree } from '../adapters/backendMenuAdapter'
import { formatMenuTitle } from '@/utils'
class MenuProcessor {
/**
@@ -36,7 +37,7 @@
*/
async processBackendMenu() {
const list = await fetchGetMenuList()
- return this.filterEmptyMenus(list)
+ return adaptBackendMenuTree(list)
}
/**
* 鏍规嵁瑙掕壊杩囨护鑿滃崟
diff --git a/rsf-design/src/router/guards/beforeEach.js b/rsf-design/src/router/guards/beforeEach.js
index f3e37d4..d49d94a 100644
--- a/rsf-design/src/router/guards/beforeEach.js
+++ b/rsf-design/src/router/guards/beforeEach.js
@@ -10,7 +10,7 @@
import { loadingService } from '@/utils/ui'
import { useCommon } from '@/hooks/core/useCommon'
import { useWorktabStore } from '@/store/modules/worktab'
-import { fetchGetUserInfo } from '@/api/auth'
+import { fetchGetUserInfo, normalizeUserInfo } from '@/api/auth'
import { ApiStatus } from '@/utils/http/status'
import { isHttpError } from '@/utils/http/error'
import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core'
@@ -185,7 +185,7 @@
}
async function fetchUserInfo() {
const userStore = useUserStore()
- const data = await fetchGetUserInfo()
+ const data = normalizeUserInfo(await fetchGetUserInfo())
userStore.setUserInfo(data)
userStore.checkAndClearWorktabs()
}
diff --git a/rsf-design/src/store/modules/user.js b/rsf-design/src/store/modules/user.js
index 19839ae..6c471b3 100644
--- a/rsf-design/src/store/modules/user.js
+++ b/rsf-design/src/store/modules/user.js
@@ -41,11 +41,9 @@
const setLockPassword = (password) => {
lockPassword.value = password
}
- const setToken = (newAccessToken, newRefreshToken) => {
- accessToken.value = newAccessToken
- if (newRefreshToken) {
- refreshToken.value = newRefreshToken
- }
+ const setToken = (newAccessToken, newRefreshToken = '') => {
+ accessToken.value = newAccessToken || ''
+ refreshToken.value = newRefreshToken || ''
}
const logOut = () => {
const currentUserId = info.value.userId
diff --git a/rsf-design/src/views/auth/login/index.vue b/rsf-design/src/views/auth/login/index.vue
index f3e4a2f..1f6abd5 100644
--- a/rsf-design/src/views/auth/login/index.vue
+++ b/rsf-design/src/views/auth/login/index.vue
@@ -18,18 +18,6 @@
@keyup.enter="handleSubmit"
style="margin-top: 25px"
>
- <ElFormItem prop="account">
- <ElSelect v-model="formData.account" @change="setupAccount">
- <ElOption
- v-for="account in accounts"
- :key="account.key"
- :label="account.label"
- :value="account.key"
- >
- <span>{{ account.label }}</span>
- </ElOption>
- </ElSelect>
- </ElFormItem>
<ElFormItem prop="username">
<ElInput
class="custom-height"
@@ -47,31 +35,6 @@
show-password
/>
</ElFormItem>
-
- <!-- 鎺ㄦ嫿楠岃瘉 -->
- <div class="relative pb-5 mt-6">
- <div
- class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
- :class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }"
- >
- <ArtDragVerify
- ref="dragVerify"
- v-model:value="isPassing"
- :text="$t('login.sliderText')"
- textColor="var(--art-gray-700)"
- :successText="$t('login.sliderSuccessText')"
- progressBarBg="var(--main-color)"
- :background="isDark ? '#26272F' : '#F1F1F4'"
- handlerBg="var(--default-box-color)"
- />
- </div>
- <p
- class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
- :class="{ 'translate-y-10': !isPassing && isClickPass }"
- >
- {{ $t('login.placeholder.slider') }}
- </p>
- </div>
<div class="flex-cb mt-2 text-sm">
<ElCheckbox v-model="formData.rememberPassword">{{
@@ -111,51 +74,20 @@
import AppConfig from '@/config'
import { useUserStore } from '@/store/modules/user'
import { useI18n } from 'vue-i18n'
- import { HttpError } from '@/utils/http/error'
- import { fetchLogin } from '@/api/auth'
+ import { fetchGetUserInfo, fetchLogin, normalizeLoginResponse } from '@/api/auth'
import { ElNotification } from 'element-plus'
- import { useSettingStore } from '@/store/modules/setting'
defineOptions({ name: 'Login' })
- const settingStore = useSettingStore()
- const { isDark } = storeToRefs(settingStore)
const { t, locale } = useI18n()
const formKey = ref(0)
watch(locale, () => {
formKey.value++
})
- const accounts = computed(() => [
- {
- key: 'super',
- label: t('login.roles.super'),
- userName: 'Super',
- password: '123456',
- roles: ['R_SUPER']
- },
- {
- key: 'admin',
- label: t('login.roles.admin'),
- userName: 'Admin',
- password: '123456',
- roles: ['R_ADMIN']
- },
- {
- key: 'user',
- label: t('login.roles.user'),
- userName: 'User',
- password: '123456',
- roles: ['R_USER']
- }
- ])
- const dragVerify = ref()
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
- const isPassing = ref(false)
- const isClickPass = ref(false)
const systemName = AppConfig.systemInfo.name
const formRef = ref()
const formData = reactive({
- account: '',
username: '',
password: '',
rememberPassword: true
@@ -165,49 +97,27 @@
password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }]
}))
const loading = ref(false)
- onMounted(() => {
- setupAccount('super')
- })
- const setupAccount = (key) => {
- const selectedAccount = accounts.value.find((account) => account.key === key)
- formData.account = key
- formData.username = selectedAccount?.userName ?? ''
- formData.password = selectedAccount?.password ?? ''
- }
const handleSubmit = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (!valid) return
- if (!isPassing.value) {
- isClickPass.value = true
- return
- }
loading.value = true
- const { username, password } = formData
- const { token, refreshToken } = await fetchLogin({
- userName: username,
- password
- })
- if (!token) {
- throw new Error('Login failed - no token received')
- }
- userStore.setToken(token, refreshToken)
+ const payload = normalizeLoginResponse(await fetchLogin(formData))
+ if (!payload.accessToken) return
+ userStore.setToken(payload.accessToken, payload.refreshToken)
userStore.setLoginStatus(true)
- showLoginSuccessNotice()
- const redirect = route.query.redirect
- router.push(redirect || '/')
- } catch (error) {
- if (!(error instanceof HttpError)) {
- console.error('[Login] Unexpected error:', error)
+ const userInfo = await fetchGetUserInfo()
+ if (userInfo && Object.keys(userInfo).length > 0) {
+ userStore.setUserInfo(userInfo)
+ } else if (payload.user && Object.keys(payload.user).length > 0) {
+ userStore.setUserInfo(payload.user)
}
+ showLoginSuccessNotice()
+ router.push(route.query.redirect || '/')
} finally {
loading.value = false
- resetDragVerify()
}
- }
- const resetDragVerify = () => {
- dragVerify.value.reset()
}
const showLoginSuccessNotice = () => {
setTimeout(() => {
@@ -224,10 +134,4 @@
<style scoped>
@import './style.css';
-</style>
-
-<style lang="scss" scoped>
- :deep(.el-select__wrapper) {
- height: 40px !important;
- }
</style>
diff --git a/rsf-design/src/views/dashboard/console/index.vue b/rsf-design/src/views/dashboard/console/index.vue
index 39fa3be..90691e1 100644
--- a/rsf-design/src/views/dashboard/console/index.vue
+++ b/rsf-design/src/views/dashboard/console/index.vue
@@ -1,44 +1,249 @@
-<!-- 宸ヤ綔鍙伴〉闈� -->
<template>
- <div>
- <CardList></CardList>
+ <div class="art-full-height flex flex-col gap-5">
+ <section
+ class="overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,var(--art-main-bg-color),var(--art-card-bg-color))] p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)]"
+ >
+ <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
+ <div class="max-w-3xl">
+ <div
+ class="mb-3 inline-flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-600"
+ >
+ <span class="size-2 rounded-full bg-emerald-500"></span>
+ RSF Phase 1 Landing
+ </div>
+ <h1
+ class="m-0 text-3xl font-semibold tracking-tight text-[var(--art-gray-900)] md:text-4xl"
+ >
+ 杩愯楠ㄦ灦宸茬粡鍒囧埌 `rsf-design`
+ </h1>
+ <p class="mt-4 max-w-2xl text-sm leading-7 text-[var(--art-gray-600)] md:text-base">
+ 褰撳墠鍏ュ彛宸茬粡鎺ュ叆鐪熷疄鍚庣鐧诲綍銆佸姩鎬佽彍鍗曞拰鏉冮檺閾捐矾銆傝繖涓椤靛彧灞曠ず宸茬粡鍙敤鐨� phase-1
+ 鑳藉姏锛屼笉鍐嶄繚鐣欐ā鏉块噷鐨勭ず渚嬪浘琛ㄥ拰婕旂ず鏁版嵁銆�
+ </p>
+ </div>
- <ElRow :gutter="20">
- <ElCol :sm="24" :md="12" :lg="10">
- <ActiveUser />
- </ElCol>
- <ElCol :sm="24" :md="12" :lg="14">
- <SalesOverview />
- </ElCol>
- </ElRow>
+ <div class="rounded-2xl border border-white/10 bg-white/70 px-4 py-3 backdrop-blur-sm">
+ <p class="text-xs uppercase tracking-[0.24em] text-[var(--art-gray-500)]"
+ >Current Entry</p
+ >
+ <p class="mt-2 text-lg font-semibold text-[var(--art-gray-900)]">
+ {{ currentUserName }}
+ </p>
+ <p class="mt-1 text-sm text-[var(--art-gray-600)]">
+ {{ currentUserRoleText }} 路 {{ currentMenuLabel }}
+ </p>
+ </div>
+ </div>
+ </section>
- <ElRow :gutter="20">
- <ElCol :sm="24" :md="24" :lg="12">
- <NewUser />
- </ElCol>
- <ElCol :sm="24" :md="12" :lg="6">
- <Dynamic />
- </ElCol>
- <ElCol :sm="24" :md="12" :lg="6">
- <TodoList />
- </ElCol>
- </ElRow>
+ <section class="grid gap-4 md:grid-cols-3">
+ <ArtStatsCard
+ title="宸叉帴鍏ュ悗绔�"
+ :count="backendSwitchCount"
+ description="鐧诲綍銆佺敤鎴蜂俊鎭�佽彍鍗曞叏閮ㄦ潵鑷� rsf-server"
+ icon="ri:server-line"
+ />
+ <ArtStatsCard
+ title="鍔ㄦ�佽彍鍗�"
+ :count="visibleMenuCount"
+ description="浠呭彂甯� phase-1 鍏佽杩涘叆鐨勬柊鍏ュ彛"
+ icon="ri:route-line"
+ />
+ <ArtStatsCard
+ title="鏉冮檺閾捐矾"
+ :count="permissionSignalCount"
+ description="瑙掕壊涓庢潈闄愯妭鐐瑰凡浠庣湡瀹炵敤鎴锋暟鎹仮澶�"
+ icon="ri:shield-check-line"
+ />
+ </section>
- <AboutProject />
+ <section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
+ <ElCard
+ class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
+ shadow="never"
+ >
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>
+ <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鐪熷疄杩愯鐘舵��</h2>
+ <p class="mt-1 text-xs text-[var(--art-gray-500)]">
+ 杩欎簺淇℃伅鏉ヨ嚜褰撳墠鐧诲綍鐢ㄦ埛鍜岃彍鍗� store
+ </p>
+ </div>
+ <ElTag type="success" effect="light">Backend mode</ElTag>
+ </div>
+ </template>
+
+ <div class="grid gap-4 md:grid-cols-2">
+ <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
+ <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">User</p>
+ <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
+ {{ currentUserName }}
+ </p>
+ <p class="mt-2 text-sm text-[var(--art-gray-600)]">
+ {{ currentUserRoleText }}
+ </p>
+ <div class="mt-4 flex flex-wrap gap-2">
+ <ElTag v-for="role in currentRoles" :key="role" type="info" effect="plain">
+ {{ role }}
+ </ElTag>
+ <ElTag v-if="!currentRoles.length" type="info" effect="plain">No roles</ElTag>
+ </div>
+ </div>
+
+ <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
+ <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">Permissions</p>
+ <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
+ {{ currentAuthorities.length }} auth nodes
+ </p>
+ <p class="mt-2 text-sm text-[var(--art-gray-600)]">
+ 鏉冮檺鑺傜偣鐩存帴鏉ヨ嚜褰撳墠鐢ㄦ埛鐨勭湡瀹� `authorities` 杞借嵎锛屼笉鍐嶄緷璧栨ā鏉挎紨绀烘�併��
+ </p>
+ <div class="mt-4 flex flex-wrap gap-2">
+ <ElTag v-for="item in previewAuthorities" :key="item" type="warning" effect="plain">
+ {{ item }}
+ </ElTag>
+ <ElTag v-if="!previewAuthorities.length" type="warning" effect="plain">
+ No authorities
+ </ElTag>
+ </div>
+ </div>
+ </div>
+ </ElCard>
+
+ <ElCard
+ class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
+ shadow="never"
+ >
+ <template #header>
+ <div>
+ <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鑿滃崟鎺ュ叆娓呭崟</h2>
+ <p class="mt-1 text-xs text-[var(--art-gray-500)]">
+ 褰撳墠鑿滃崟鏍戠敤浜庨獙璇� `rsf-design` 鏄惁鐪熸鎺ヤ綇鍚庣鍙戝竷
+ </p>
+ </div>
+ </template>
+
+ <div class="space-y-3">
+ <div
+ v-for="item in menuPreview"
+ :key="item.key"
+ class="flex items-center justify-between rounded-2xl border border-[var(--art-gray-200)] px-4 py-3"
+ >
+ <div>
+ <p class="text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <p class="mt-1 text-xs text-[var(--art-gray-500)]">{{ item.description }}</p>
+ </div>
+ <ElTag :type="item.type" effect="light">
+ {{ item.value }}
+ </ElTag>
+ </div>
+ </div>
+ </ElCard>
+ </section>
</div>
</template>
<script setup>
- import CardList from './modules/card-list.vue'
- import ActiveUser from './modules/active-user.vue'
-
- import SalesOverview from './modules/sales-overview.vue'
- import NewUser from './modules/new-user.vue'
-
- import Dynamic from './modules/dynamic-stats.vue'
- import TodoList from './modules/todo-list.vue'
-
- import AboutProject from './modules/about-project.vue'
+ import { storeToRefs } from 'pinia'
+ import { useUserStore } from '@/store/modules/user'
+ import { useMenuStore } from '@/store/modules/menu'
+ import { formatMenuTitle } from '@/utils/router'
defineOptions({ name: 'Console' })
+
+ const userStore = useUserStore()
+ const menuStore = useMenuStore()
+ const { getUserInfo } = storeToRefs(userStore)
+ const { menuList } = storeToRefs(menuStore)
+
+ const currentUser = computed(() => getUserInfo.value || {})
+ const currentUserName = computed(() => {
+ return (
+ currentUser.value.userName ||
+ currentUser.value.username ||
+ currentUser.value.nickname ||
+ 'RSF User'
+ )
+ })
+ const currentRoles = computed(() => {
+ const roles = currentUser.value.roles
+ if (!Array.isArray(roles)) return []
+ return roles
+ .map((role) => {
+ if (typeof role === 'string') {
+ return role
+ }
+ return role?.code || role?.name || role?.title || ''
+ })
+ .filter(Boolean)
+ })
+ const currentAuthorities = computed(() => {
+ const authorities = currentUser.value.authorities
+ if (!Array.isArray(authorities)) return []
+ return authorities.map((item) => item?.authority || '').filter(Boolean)
+ })
+ const currentMenuLabel = computed(() => {
+ const firstMenu = menuList.value?.[0]
+ return formatMenuTitle(firstMenu?.meta?.title || 'menus.dashboard.console')
+ })
+ const currentUserRoleText = computed(() => {
+ if (!currentRoles.value.length) {
+ return 'No role information yet'
+ }
+ return `Roles: ${currentRoles.value.join(' / ')}`
+ })
+ const backendSwitchCount = computed(() => {
+ return currentUserName.value !== 'RSF User' || menuList.value.length > 0 ? 1 : 0
+ })
+ const visibleMenuCount = computed(() => countVisibleMenus(menuList.value))
+ const permissionSignalCount = computed(() => {
+ return currentRoles.value.length + currentAuthorities.value.length
+ })
+ const previewAuthorities = computed(() => currentAuthorities.value.slice(0, 4))
+ const menuPreview = computed(() => {
+ const total = visibleMenuCount.value
+ const rootCount = Array.isArray(menuList.value) ? menuList.value.length : 0
+ const firstMenu = menuList.value?.[0]
+ const firstChildren = Array.isArray(firstMenu?.children) ? firstMenu.children.length : 0
+
+ return [
+ {
+ key: 'entry',
+ title: '鍏ュ彛妯″紡',
+ description: '褰撳墠椤甸潰浠� backend mode 杩愯锛岃彍鍗曠敱鏈嶅姟绔┍鍔�',
+ value: 'backend',
+ type: 'success'
+ },
+ {
+ key: 'menus',
+ title: '鍙鑿滃崟',
+ description: '宸查�氳繃鍔ㄦ�佽矾鐢遍�傞厤鍚庤繘鍏ュ墠绔彍鍗曟爲',
+ value: `${total}`,
+ type: 'primary'
+ },
+ {
+ key: 'root',
+ title: '涓�绾х洰褰�',
+ description: '鏍圭骇鑿滃崟鑺傜偣鏁伴噺',
+ value: `${rootCount}`,
+ type: 'info'
+ },
+ {
+ key: 'children',
+ title: '棣栦釜鐩綍瀛愰」',
+ description: '鐢ㄤ簬蹇�熺‘璁よ彍鍗曟爲宸茶姝g‘灞曞紑',
+ value: `${firstChildren}`,
+ type: 'warning'
+ }
+ ]
+ })
+
+ function countVisibleMenus(items) {
+ if (!Array.isArray(items)) return 0
+ return items.reduce((total, item) => {
+ const current = item?.meta?.isHide ? 0 : 1
+ return total + current + countVisibleMenus(item?.children)
+ }, 0)
+ }
</script>
diff --git a/rsf-design/src/views/system/role/index.vue b/rsf-design/src/views/system/role/index.vue
index d6a11ca..b6787aa 100644
--- a/rsf-design/src/views/system/role/index.vue
+++ b/rsf-design/src/views/system/role/index.vue
@@ -5,8 +5,8 @@
v-show="showSearchBar"
v-model="searchForm"
@search="handleSearch"
- @reset="resetSearchParams"
- ></RoleSearch>
+ @reset="handleReset"
+ />
<ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
<ArtTableHeader
@@ -17,62 +17,115 @@
>
<template #left>
<ElSpace wrap>
- <ElButton @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <span v-auth="'query'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="roleReportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
</ElSpace>
</template>
</ArtTableHeader>
- <!-- 琛ㄦ牸 -->
<ArtTable
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
+ @selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
- >
- </ArtTable>
+ />
</ElCard>
- <!-- 瑙掕壊缂栬緫寮圭獥 -->
<RoleEditDialog
- v-model="dialogVisible"
+ v-model:visible="dialogVisible"
:dialog-type="dialogType"
:role-data="currentRoleData"
- @success="refreshData"
+ @submit="handleDialogSubmit"
/>
- <!-- 鑿滃崟鏉冮檺寮圭獥 -->
<RolePermissionDialog
- v-model="permissionDialog"
+ v-model:visible="permissionDialogVisible"
:role-data="currentRoleData"
+ :scope-type="permissionScopeType"
@success="refreshData"
/>
</div>
</template>
<script setup>
+ import { useUserStore } from '@/store/modules/user'
+ import {
+ fetchExportRoleReport,
+ fetchDeleteRole,
+ fetchGetRoleMany,
+ fetchRolePrintPage,
+ fetchRolePage,
+ fetchSaveRole,
+ fetchUpdateRole
+ } from '@/api/system-manage'
+ import { useTable } from '@/hooks/core/useTable'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
import RoleSearch from './modules/role-search.vue'
import RoleEditDialog from './modules/role-edit-dialog.vue'
-
import RolePermissionDialog from './modules/role-permission-dialog.vue'
-
- import { useTable } from '@/hooks/core/useTable'
- import { fetchGetRoleList } from '@/api/system-manage'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
- import { ElTag, ElMessageBox } from 'element-plus'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
+ import {
+ buildRoleDialogModel,
+ buildRolePageQueryParams,
+ buildRolePrintRows,
+ buildRoleReportMeta,
+ buildRoleSavePayload,
+ buildRoleSearchParams,
+ createRoleSearchState,
+ getRoleStatusMeta,
+ normalizeRoleListRow,
+ ROLE_REPORT_STYLE,
+ ROLE_REPORT_TITLE,
+ resolveRoleReportColumns
+ } from './rolePage.helpers'
+
defineOptions({ name: 'Role' })
- const searchForm = ref({
- roleName: void 0,
- roleCode: void 0,
- description: void 0,
- enabled: void 0,
- daterange: void 0
- })
+
+ const searchForm = ref(createRoleSearchState())
const showSearchBar = ref(false)
const dialogVisible = ref(false)
- const permissionDialog = ref(false)
- const currentRoleData = ref(void 0)
+ const dialogType = ref('add')
+ const currentRoleData = ref(buildRoleDialogModel())
+ const permissionDialogVisible = ref(false)
+ const permissionScopeType = ref('menu')
+ const selectedRows = ref([])
+ const previewVisible = ref(false)
+ const previewRows = ref([])
+ const previewMeta = ref({})
+ const previewToken = ref(0)
+ const activePrintToken = ref(0)
+ const userStore = useUserStore()
+ const reportTitle = ROLE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildRoleSearchParams(searchForm.value))
+
const {
columns,
columnChecks,
@@ -84,130 +137,313 @@
resetSearchParams,
handleSizeChange,
handleCurrentChange,
- refreshData
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
} = useTable({
- // 鏍稿績閰嶇疆
core: {
- apiFn: fetchGetRoleList,
- apiParams: {
- current: 1,
- size: 20
- },
- // 鎺掗櫎 apiParams 涓殑灞炴��
- excludeParams: ['daterange'],
+ apiFn: fetchRolePage,
+ apiParams: buildRolePageQueryParams(searchForm.value),
columnsFactory: () => [
+ { type: 'selection', width: 52, fixed: 'left' },
{
- prop: 'roleId',
- label: '瑙掕壊ID',
- width: 100
- },
- {
- prop: 'roleName',
+ prop: 'name',
label: '瑙掕壊鍚嶇О',
- minWidth: 120
- },
- {
- prop: 'roleCode',
- label: '瑙掕壊缂栫爜',
- minWidth: 120
- },
- {
- prop: 'description',
- label: '瑙掕壊鎻忚堪',
- minWidth: 150,
+ minWidth: 140,
showOverflowTooltip: true
},
{
- prop: 'enabled',
- label: '瑙掕壊鐘舵��',
- width: 100,
+ prop: 'code',
+ label: '瑙掕壊缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 120,
formatter: (row) => {
- const statusConfig = row.enabled
- ? { type: 'success', text: '鍚敤' }
- : { type: 'warning', text: '绂佺敤' }
- return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
+ const statusMeta = getRoleStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
}
},
{
- prop: 'createTime',
- label: '鍒涘缓鏃ユ湡',
- width: 180,
- sortable: true
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ sortable: true,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ sortable: true,
+ formatter: (row) => row.createTimeText || '-'
},
{
prop: 'operation',
label: '鎿嶄綔',
- width: 80,
+ width: 120,
fixed: 'right',
formatter: (row) =>
h('div', [
h(ArtButtonMore, {
list: [
{
- key: 'permission',
- label: '鑿滃崟鏉冮檺',
- icon: 'ri:user-3-line'
+ key: 'scope-menu',
+ label: '缃戦〉鏉冮檺',
+ icon: 'ri:layout-2-line',
+ auth: 'edit'
+ },
+ {
+ key: 'scope-pda',
+ label: 'PDA鏉冮檺',
+ icon: 'ri:smartphone-line',
+ auth: 'edit'
+ },
+ {
+ key: 'scope-matnr',
+ label: '鐗╂枡鏉冮檺',
+ icon: 'ri:archive-line',
+ auth: 'edit'
+ },
+ {
+ key: 'scope-warehouse',
+ label: '浠撳簱鏉冮檺',
+ icon: 'ri:store-2-line',
+ auth: 'edit'
},
{
key: 'edit',
label: '缂栬緫瑙掕壊',
- icon: 'ri:edit-2-line'
+ icon: 'ri:edit-2-line',
+ auth: 'edit'
},
{
key: 'delete',
label: '鍒犻櫎瑙掕壊',
icon: 'ri:delete-bin-4-line',
- color: '#f56c6c'
+ color: '#f56c6c',
+ auth: 'delete'
}
],
- onClick: (item) => buttonMoreClick(item, row)
+ onClick: (item) => handleActionClick(item, row)
})
])
}
]
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeRoleListRow(item))
+ }
}
})
- const dialogType = ref('add')
- const showDialog = (type, row) => {
- dialogVisible.value = true
- dialogType.value = type
- currentRoleData.value = row
- }
+
+ const roleReportColumns = computed(() => resolveRoleReportColumns(columns.value))
+ const resolvedPreviewMeta = computed(() =>
+ buildRoleReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ titleAlign: ROLE_REPORT_STYLE.titleAlign,
+ titleLevel: ROLE_REPORT_STYLE.titleLevel
+ })
+ )
+
const handleSearch = (params) => {
- const { daterange, ...filtersParams } = params
- const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
- replaceSearchParams({ ...filtersParams, startTime, endTime })
+ replaceSearchParams(buildRoleSearchParams(params))
getData()
}
- const buttonMoreClick = (item, row) => {
+
+ const handleReset = () => {
+ Object.assign(searchForm.value, createRoleSearchState())
+ resetSearchParams()
+ }
+
+ const handleSelectionChange = (rows) => {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ const handlePreviewVisibleChange = (visible) => {
+ previewVisible.value = Boolean(visible)
+ if (!visible) {
+ activePrintToken.value = 0
+ }
+ }
+
+ const showDialog = (type, row) => {
+ dialogType.value = type
+ currentRoleData.value = type === 'edit' ? buildRoleDialogModel(row) : buildRoleDialogModel()
+ dialogVisible.value = true
+ }
+
+ const handleActionClick = (item, row) => {
switch (item.key) {
- case 'permission':
- showPermissionDialog(row)
+ case 'scope-menu':
+ openScopeDialog('menu', row)
+ break
+ case 'scope-pda':
+ openScopeDialog('pda', row)
+ break
+ case 'scope-matnr':
+ openScopeDialog('matnr', row)
+ break
+ case 'scope-warehouse':
+ openScopeDialog('warehouse', row)
break
case 'edit':
showDialog('edit', row)
break
case 'delete':
- deleteRole(row)
+ handleDelete(row)
+ break
+ default:
break
}
}
- const showPermissionDialog = (row) => {
- permissionDialog.value = true
- currentRoleData.value = row
+
+ const openScopeDialog = (scopeType, row) => {
+ permissionScopeType.value = scopeType
+ currentRoleData.value = buildRoleDialogModel(row)
+ permissionDialogVisible.value = true
}
- const deleteRole = (row) => {
- ElMessageBox.confirm(`纭畾鍒犻櫎瑙掕壊"${row.roleName}"鍚楋紵姝ゆ搷浣滀笉鍙仮澶嶏紒`, '鍒犻櫎纭', {
- confirmButtonText: '纭畾',
- cancelButtonText: '鍙栨秷',
- type: 'warning'
- })
- .then(() => {
- ElMessage.success('鍒犻櫎鎴愬姛')
- refreshData()
+
+ const handleDialogSubmit = async (formData) => {
+ const payload = buildRoleSavePayload(formData)
+ try {
+ if (dialogType.value === 'edit') {
+ await fetchUpdateRole(payload)
+ ElMessage.success('淇敼鎴愬姛')
+ dialogVisible.value = false
+ currentRoleData.value = buildRoleDialogModel()
+ await refreshUpdate()
+ return
+ }
+ await fetchSaveRole(payload)
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ currentRoleData.value = buildRoleDialogModel()
+ await refreshCreate()
+ } catch (error) {
+ ElMessage.error(error?.message || '鎻愪氦澶辫触')
+ }
+ }
+
+ const handleDelete = async (row) => {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄よ鑹层��${row.name || row.code || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
})
- .catch(() => {
- ElMessage.info('宸插彇娑堝垹闄�')
+ await fetchDeleteRole(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 fetchDeleteRole(ids.join(','))
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+ selectedRows.value = []
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鎵归噺鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ const handleExport = async (payload) => {
+ try {
+ const response = await fetchExportRoleReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ })
+ 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 = 'role.xlsx'
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ window.URL.revokeObjectURL(downloadUrl)
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ } catch (error) {
+ ElMessage.error(error?.message || '瀵煎嚭澶辫触')
+ }
+ }
+
+ const handlePrint = async (payload) => {
+ const token = previewToken.value + 1
+ previewToken.value = token
+ activePrintToken.value = token
+ previewVisible.value = false
+ previewRows.value = []
+ previewMeta.value = {}
+
+ try {
+ const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGetRoleMany(payload.ids)
+ : await fetchRolePrintPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ if (activePrintToken.value !== token) {
+ return
+ }
+ const records = defaultResponseAdapter(response).records
+ if (activePrintToken.value !== token) {
+ return
+ }
+
+ const rows = buildRolePrintRows(records)
+ const now = new Date()
+ previewRows.value = rows
+ previewMeta.value = {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ handlePreviewVisibleChange(true)
+ } catch (error) {
+ if (activePrintToken.value !== token) {
+ return
+ }
+ ElMessage.error(error?.message || '鎵撳嵃澶辫触')
+ }
}
</script>
diff --git a/rsf-design/src/views/system/role/modules/role-search.vue b/rsf-design/src/views/system/role/modules/role-search.vue
index afcb688..a8838ab 100644
--- a/rsf-design/src/views/system/role/modules/role-search.vue
+++ b/rsf-design/src/views/system/role/modules/role-search.vue
@@ -3,84 +3,85 @@
ref="searchBarRef"
v-model="formData"
:items="formItems"
- :rules="rules"
+ :showExpand="false"
@reset="handleReset"
@search="handleSearch"
- >
- </ArtSearchBar>
+ />
</template>
<script setup>
+ import { createRoleSearchState } from '../rolePage.helpers'
+
const props = defineProps({
modelValue: { required: true }
})
+
const emit = defineEmits(['update:modelValue', 'search', 'reset'])
const searchBarRef = ref()
+
const formData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
- const rules = {}
- const statusOptions = ref([
- { label: '鍚敤', value: true },
- { label: '绂佺敤', value: false }
- ])
+
const formItems = computed(() => [
{
label: '瑙掕壊鍚嶇О',
- key: 'roleName',
+ key: 'name',
type: 'input',
- placeholder: '璇疯緭鍏ヨ鑹插悕绉�',
- clearable: true
- },
- {
- label: '瑙掕壊缂栫爜',
- key: 'roleCode',
- type: 'input',
- placeholder: '璇疯緭鍏ヨ鑹茬紪鐮�',
- clearable: true
- },
- {
- label: '瑙掕壊鎻忚堪',
- key: 'description',
- type: 'input',
- placeholder: '璇疯緭鍏ヨ鑹叉弿杩�',
- clearable: true
- },
- {
- label: '瑙掕壊鐘舵��',
- key: 'enabled',
- type: 'select',
props: {
- placeholder: '璇烽�夋嫨鐘舵��',
- options: statusOptions.value,
+ placeholder: '璇疯緭鍏ヨ鑹插悕绉�',
clearable: true
}
},
{
- label: '鍒涘缓鏃ユ湡',
- key: 'daterange',
- type: 'datetime',
+ label: '瑙掕壊缂栫爜',
+ key: 'code',
+ type: 'input',
props: {
- style: { width: '100%' },
- placeholder: '璇烽�夋嫨鏃ユ湡鑼冨洿',
- type: 'daterange',
- rangeSeparator: '鑷�',
- startPlaceholder: '寮�濮嬫棩鏈�',
- endPlaceholder: '缁撴潫鏃ユ湡',
- valueFormat: 'YYYY-MM-DD',
- shortcuts: [
- { text: '浠婃棩', value: [/* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()] },
- { text: '鏈�杩戜竴鍛�', value: [new Date(Date.now() - 6048e5), /* @__PURE__ */ new Date()] },
- { text: '鏈�杩戜竴涓湀', value: [new Date(Date.now() - 2592e6), /* @__PURE__ */ new Date()] }
+ placeholder: '璇疯緭鍏ヨ鑹茬紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ placeholder: '杈撳叆鍏抽敭瀛楁悳绱�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '绂佺敤', value: 0 }
]
}
}
])
- const handleReset = () => {
+
+ function handleReset() {
+ emit('update:modelValue', createRoleSearchState())
emit('reset')
}
- const handleSearch = async (params) => {
+
+ async function handleSearch(params) {
await searchBarRef.value.validate()
emit('search', params)
}
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
new file mode 100644
index 0000000..77b7b81
--- /dev/null
+++ b/rsf-design/src/views/system/role/rolePage.helpers.js
@@ -0,0 +1,320 @@
+export function createRoleSearchState() {
+ return {
+ name: '',
+ code: '',
+ memo: '',
+ status: void 0,
+ condition: ''
+ }
+}
+
+export function createRoleFormState() {
+ return {
+ id: void 0,
+ name: '',
+ code: '',
+ memo: '',
+ status: 1
+ }
+}
+
+export function buildRoleSearchParams(params = {}) {
+ const searchParams = {
+ name: params.name,
+ code: params.code,
+ memo: params.memo,
+ status: params.status,
+ condition: params.condition
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildRolePageQueryParams(params = {}) {
+ const { current, size, pageSize, ...filters } = params
+ return {
+ current: current || 1,
+ pageSize: pageSize || size || 20,
+ ...buildRoleSearchParams(filters)
+ }
+}
+
+const ROLE_REPORT_COLUMNS = [
+ { source: 'name', label: '瑙掕壊鍚嶇О' },
+ { source: 'code', label: '瑙掕壊缂栫爜' },
+ { source: 'statusText', label: '鐘舵��' },
+ { source: 'memo', label: '澶囨敞' },
+ { source: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { source: 'updateTimeText', label: '鏇存柊鏃堕棿' }
+]
+
+const ROLE_REPORT_SOURCE_ALIAS = {
+ status: 'statusText'
+}
+
+export const ROLE_REPORT_TITLE = '瑙掕壊绠$悊鎶ヨ〃'
+
+export const ROLE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong'
+}
+
+export function getRoleReportColumns() {
+ return ROLE_REPORT_COLUMNS.map((column) => ({ ...column }))
+}
+
+export function resolveRoleReportColumns(columns = []) {
+ if (!Array.isArray(columns)) {
+ return []
+ }
+
+ const allowedColumns = new Map(ROLE_REPORT_COLUMNS.map((column) => [column.source, column]))
+ const seenSources = new Set()
+
+ return columns
+ .map((column) => {
+ if (!column || typeof column !== 'object') {
+ return null
+ }
+
+ const source = ROLE_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ?? column.source ?? column.prop
+ if (!source || !allowedColumns.has(source) || seenSources.has(source)) {
+ return null
+ }
+
+ seenSources.add(source)
+ const allowedColumn = allowedColumns.get(source)
+ return {
+ source,
+ label: column.label || allowedColumn.label
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildRolePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records.map((record) => normalizeRoleListRow(record))
+}
+
+export function buildRoleReportMeta({
+ previewMeta = {},
+ count = 0,
+ titleAlign = ROLE_REPORT_STYLE.titleAlign,
+ titleLevel = ROLE_REPORT_STYLE.titleLevel
+} = {}) {
+ return {
+ reportTitle: ROLE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ titleAlign,
+ titleLevel,
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+ }
+ }
+}
+
+export function buildRoleDialogModel(record = {}) {
+ return {
+ ...createRoleFormState(),
+ id: normalizeRoleId(record.id),
+ name: record.name || '',
+ code: record.code || '',
+ memo: record.memo || '',
+ status: record.status !== void 0 && record.status !== null ? record.status : 1
+ }
+}
+
+export function buildRoleSavePayload(form = {}) {
+ return {
+ id: normalizeRoleId(form.id),
+ name: form.name || '',
+ code: form.code || '',
+ memo: form.memo || '',
+ status: form.status !== void 0 && form.status !== null ? form.status : 1
+ }
+}
+
+export function normalizeRoleListRow(record = {}) {
+ const statusMeta = getRoleStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ createTimeText: record.createTime$ || record.createTime || '',
+ updateTimeText: record.updateTime$ || record.updateTime || ''
+ }
+}
+
+export function getRoleStatusMeta(status) {
+ if (status === true || status === 1) {
+ return { type: 'success', text: '姝e父', bool: true }
+ }
+ if (status === false || status === 0) {
+ return { type: 'danger', text: '绂佺敤', bool: false }
+ }
+ return { type: 'info', text: '鏈煡', bool: false }
+}
+
+export function getRoleScopeConfig(scopeType) {
+ const configMap = {
+ menu: {
+ scopeType: 'menu',
+ title: '缃戦〉鏉冮檺',
+ listUrl: '/role/scope/list',
+ treeUrl: '/menu/tree'
+ },
+ pda: {
+ scopeType: 'pda',
+ title: 'PDA鏉冮檺',
+ listUrl: '/rolePda/scope/list',
+ treeUrl: '/menuPda/tree'
+ },
+ matnr: {
+ scopeType: 'matnr',
+ title: '鐗╂枡鏉冮檺',
+ listUrl: '/roleMatnr/scope/list',
+ treeUrl: '/menuMatnrGroup/tree'
+ },
+ warehouse: {
+ scopeType: 'warehouse',
+ title: '浠撳簱鏉冮檺',
+ listUrl: '/roleWarehouse/scope/list',
+ treeUrl: '/menuWarehouse/tree'
+ }
+ }
+
+ const config = configMap[scopeType]
+ if (!config) {
+ throw new Error(`Unsupported scope type: ${scopeType}`)
+ }
+ return config
+}
+
+export function buildRoleScopeSubmitPayload(roleId, checkedKeys = [], halfCheckedKeys = []) {
+ return {
+ id: normalizeRoleId(roleId),
+ menuIds: {
+ checked: normalizeScopeKeys(checkedKeys),
+ halfChecked: normalizeScopeKeys(halfCheckedKeys)
+ }
+ }
+}
+
+export function normalizeRoleScopeTreeData(scopeType, treeData = []) {
+ if (!Array.isArray(treeData)) {
+ return []
+ }
+
+ return treeData
+ .map((node) => normalizeRoleScopeNode(scopeType, node))
+ .filter(Boolean)
+}
+
+function normalizeRoleScopeNode(scopeType, node) {
+ if (!node || typeof node !== 'object') {
+ return null
+ }
+
+ if (scopeType === 'menu' && node.type === 1) {
+ return buildScopeAuthNode(node)
+ }
+
+ const children = Array.isArray(node.children)
+ ? normalizeRoleScopeTreeData(scopeType, node.children)
+ : []
+ const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node
+ const authNodes = 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 || ''),
+ type: 1,
+ isAuthButton: true,
+ authMark: auth.authMark || auth.authority || auth.code || '',
+ children: []
+ }))
+ : []
+
+ const mergedChildren =
+ authNodes.length > 0 && !children.some((child) => child.isAuthButton)
+ ? [...children, ...authNodes]
+ : children
+
+ return {
+ id: normalizeScopeKey(node.id ?? node.value),
+ label: normalizeScopeTitle(
+ node.label || node.title || node.name || metaSource.title || node.code || ''
+ ),
+ type: node.type,
+ path: node.path || '',
+ component: node.component || '',
+ isAuthButton: Boolean(node.isAuthButton),
+ authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '',
+ meta: metaSource,
+ children: mergedChildren
+ }
+}
+
+function buildScopeAuthNode(node) {
+ 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 || ''),
+ type: 1,
+ isAuthButton: true,
+ authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '',
+ children: []
+ }
+}
+
+function normalizeScopeKeys(keys = []) {
+ if (!Array.isArray(keys)) {
+ return []
+ }
+
+ return Array.from(
+ new Set(
+ keys
+ .map((key) => normalizeRoleId(key))
+ .filter((key) => key !== void 0)
+ )
+ )
+}
+
+function normalizeScopeKey(value) {
+ const normalized = normalizeRoleId(value)
+ return normalized === void 0 ? '' : String(normalized)
+}
+
+function normalizeScopeTitle(title) {
+ if (typeof title !== 'string') {
+ return ''
+ }
+ const trimmedTitle = title.trim()
+ if (trimmedTitle.startsWith('menu.')) {
+ return `menus.${trimmedTitle.slice('menu.'.length)}`
+ }
+ return trimmedTitle
+}
+
+function normalizeRoleId(value) {
+ if (value === '' || value === null || value === void 0) {
+ return void 0
+ }
+ const numeric = Number(value)
+ if (Number.isNaN(numeric)) {
+ return value
+ }
+ return numeric
+}
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index bf169fd..634f8c9 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/rsf-design/src/views/system/user/index.vue
@@ -1,81 +1,104 @@
<!-- 鐢ㄦ埛绠$悊椤甸潰 -->
-<!-- art-full-height 鑷姩璁$畻鍑洪〉闈㈠墿浣欓珮搴� -->
-<!-- art-table-card 涓�涓鍚堢郴缁熸牱寮忕殑 class锛屽悓鏃惰嚜鍔ㄦ拺婊″墿浣欓珮搴� -->
-<!-- 鏇村 useTable 浣跨敤绀轰緥璇风Щ姝ヨ嚦 鍔熻兘绀轰緥 涓嬮潰鐨勯珮绾ц〃鏍肩ず渚嬫垨鑰呮煡鐪嬪畼鏂规枃妗� -->
-<!-- useTable 鏂囨。锛歨ttps://www.artd.pro/docs/zh/guide/hooks/use-table.html -->
<template>
<div class="user-page art-full-height">
- <!-- 鎼滅储鏍� -->
- <UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
+ <UserSearch
+ v-model="searchForm"
+ :dept-tree-options="deptTreeOptions"
+ :role-options="roleOptions"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
<ElCard class="art-table-card">
- <!-- 琛ㄦ牸澶撮儴 -->
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElSpace wrap>
- <ElButton @click="showDialog('add')" v-ripple>鏂板鐢ㄦ埛</ElButton>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鐢ㄦ埛</ElButton>
</ElSpace>
</template>
</ArtTableHeader>
- <!-- 琛ㄦ牸 -->
<ArtTable
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
- @selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
- >
- </ArtTable>
+ />
- <!-- 鐢ㄦ埛寮圭獥 -->
<UserDialog
v-model:visible="dialogVisible"
:type="dialogType"
:user-data="currentUserData"
+ :role-options="roleOptions"
+ :dept-tree-options="deptTreeOptions"
@submit="handleDialogSubmit"
+ />
+
+ <UserDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :user-data="detailUserData"
/>
</ElCard>
</div>
</template>
<script setup>
+ import request from '@/utils/http'
+ import {
+ fetchDeleteUser,
+ fetchGetDeptTree,
+ fetchGetRoleOptions,
+ fetchGetUserDetail,
+ fetchResetUserPassword,
+ fetchSaveUser,
+ fetchUpdateUser,
+ fetchUpdateUserStatus
+ } from '@/api/system-manage'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
import UserSearch from './modules/user-search.vue'
import UserDialog from './modules/user-dialog.vue'
+ import UserDetailDrawer from './modules/user-detail-drawer.vue'
+ import {
+ buildUserDialogModel,
+ buildUserPageQueryParams,
+ buildUserSavePayload,
+ buildUserSearchParams,
+ createUserSearchState,
+ getUserStatusMeta,
+ mergeUserDetailRecord,
+ normalizeDeptTreeOptions,
+ normalizeRoleOptions,
+ normalizeUserListRow,
+ formatUserRoleNames
+ } from './userPage.helpers'
- import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
- import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
- import { useTable } from '@/hooks/core/useTable'
- import { fetchGetUserList } from '@/api/system-manage'
- import { ElTag, ElMessageBox, ElImage } from 'element-plus'
defineOptions({ name: 'User' })
+
+ const searchForm = ref(createUserSearchState())
const dialogType = ref('add')
const dialogVisible = ref(false)
- const currentUserData = ref({})
- const selectedRows = ref([])
- const searchForm = ref({
- userName: void 0,
- userGender: void 0,
- userPhone: void 0,
- userEmail: void 0,
- status: '1'
- })
- const USER_STATUS_CONFIG = {
- 1: { type: 'success', text: '鍦ㄧ嚎' },
- 2: { type: 'info', text: '绂荤嚎' },
- 3: { type: 'warning', text: '寮傚父' },
- 4: { type: 'danger', text: '娉ㄩ攢' }
+ const currentUserData = ref(buildUserDialogModel())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailUserData = ref({})
+ const roleOptions = ref([])
+ const deptTreeOptions = ref([])
+ const RESET_PASSWORD = '123456'
+ const { hasAuth } = useAuth()
+
+ const fetchUserPage = (params = {}) => {
+ return request.post({
+ url: '/user/page',
+ params: buildUserPageQueryParams(params)
+ })
}
- const getUserStatusConfig = (status) => {
- return (
- USER_STATUS_CONFIG[status] || {
- type: 'info',
- text: '鏈煡'
- }
- )
- }
+
const {
columns,
columnChecks,
@@ -87,136 +110,283 @@
resetSearchParams,
handleSizeChange,
handleCurrentChange,
- refreshData
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
} = useTable({
- // 鏍稿績閰嶇疆
core: {
- apiFn: fetchGetUserList,
- apiParams: {
- current: 1,
- size: 20,
- ...searchForm.value
- },
- // 鑷畾涔夊垎椤靛瓧娈垫槧灏勶紝鏈缃椂灏嗕娇鐢ㄥ叏灞�閰嶇疆 tableConfig.ts 涓殑 paginationKey
- // paginationKey: {
- // current: 'pageNum',
- // size: 'pageSize'
- // },
+ apiFn: fetchUserPage,
+ apiParams: buildUserPageQueryParams(searchForm.value),
columnsFactory: () => [
- { type: 'selection' },
- // 鍕鹃�夊垪
- { type: 'index', width: 60, label: '搴忓彿' },
- // 搴忓彿
{
- prop: 'userInfo',
+ prop: 'username',
label: '鐢ㄦ埛鍚�',
- width: 280,
- // visible: false, // 榛樿鏄惁鏄剧ず鍒�
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'nickname',
+ label: '鏄电О',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'deptLabel',
+ label: '閮ㄩ棬',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'phone',
+ label: '鎵嬫満鍙�',
+ minWidth: 130
+ },
+ {
+ prop: 'email',
+ label: '閭',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'roleNames',
+ label: '瑙掕壊',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => formatUserRoleNames(row.roles) || row.roleNames || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 180,
formatter: (row) => {
- return h('div', { class: 'user flex-c' }, [
- h(ElImage, {
- class: 'size-9.5 rounded-md',
- src: row.avatar,
- previewSrcList: [row.avatar],
- // 鍥剧墖棰勮鏄惁鎻掑叆鑷� body 鍏冪礌涓婏紝鐢ㄤ簬瑙e喅琛ㄦ牸鍐呴儴鍥剧墖棰勮鏍峰紡寮傚父
- previewTeleported: true
+ const statusMeta = getUserStatusMeta(row.statusBool ?? row.status)
+ if (!hasAuth('edit')) {
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ return h('div', { class: 'flex items-center gap-2' }, [
+ h(ElSwitch, {
+ modelValue: row.statusBool ?? statusMeta.bool,
+ loading: row._statusLoading,
+ 'onUpdate:modelValue': (value) => handleStatusChange(row, value)
}),
- h('div', { class: 'ml-2' }, [
- h('p', { class: 'user-name' }, row.userName),
- h('p', { class: 'email' }, row.userEmail)
- ])
+ h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
])
}
},
{
- prop: 'userGender',
- label: '鎬у埆',
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
sortable: true,
- formatter: (row) => row.userGender
- },
- { prop: 'userPhone', label: '鎵嬫満鍙�' },
- {
- prop: 'status',
- label: '鐘舵��',
- formatter: (row) => {
- const statusConfig = getUserStatusConfig(row.status)
- return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
- }
+ formatter: (row) => row.updateTimeText || '-'
},
{
- prop: 'createTime',
- label: '鍒涘缓鏃ユ湡',
- sortable: true
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ sortable: true,
+ formatter: (row) => row.createTimeText || '-'
},
{
prop: 'operation',
label: '鎿嶄綔',
- width: 120,
+ width: 220,
fixed: 'right',
- // 鍥哄畾鍒�
- formatter: (row) =>
- h('div', [
- h(ArtButtonTable, {
- type: 'edit',
- onClick: () => showDialog('edit', row)
- }),
- h(ArtButtonTable, {
- type: 'delete',
- onClick: () => deleteUser(row)
- })
- ])
+ formatter: (row) => {
+ const buttons = []
+
+ if (hasAuth('query')) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => openDetail(row)
+ })
+ )
+ }
+
+ if (hasAuth('edit')) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => openEditDialog(row)
+ }),
+ h(ArtButtonTable, {
+ icon: 'ri:key-2-line',
+ iconClass: 'bg-warning/12 text-warning',
+ onClick: () => handleResetPassword(row)
+ })
+ )
+ }
+
+ if (hasAuth('delete')) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', buttons)
+ }
}
]
},
- // 鏁版嵁澶勭悊
transform: {
- // 鏁版嵁杞崲鍣� - 鏇挎崲澶村儚
dataTransformer: (records) => {
if (!Array.isArray(records)) {
- console.warn('鏁版嵁杞崲鍣�: 鏈熸湜鏁扮粍绫诲瀷锛屽疄闄呮敹鍒�:', typeof records)
return []
}
- return records.map((item, index) => {
- return {
- ...item,
- avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
- }
- })
+ return records.map((item) => normalizeUserListRow(item))
}
}
})
- const handleSearch = (params) => {
- replaceSearchParams(params)
- getData()
- }
- const showDialog = (type, row) => {
- console.log('鎵撳紑寮圭獥:', { type, row })
- dialogType.value = type
- currentUserData.value = row || {}
- nextTick(() => {
- dialogVisible.value = true
- })
- }
- const deleteUser = (row) => {
- console.log('鍒犻櫎鐢ㄦ埛:', row)
- ElMessageBox.confirm(`纭畾瑕佹敞閿�璇ョ敤鎴峰悧锛焋, '娉ㄩ攢鐢ㄦ埛', {
- confirmButtonText: '纭畾',
- cancelButtonText: '鍙栨秷',
- type: 'error'
- }).then(() => {
- ElMessage.success('娉ㄩ攢鎴愬姛')
- })
- }
- const handleDialogSubmit = async () => {
+
+ const loadLookups = async () => {
try {
- dialogVisible.value = false
- currentUserData.value = {}
+ const [roles, depts] = await Promise.all([fetchGetRoleOptions({}), fetchGetDeptTree({})])
+ roleOptions.value = normalizeRoleOptions(roles)
+ deptTreeOptions.value = normalizeDeptTreeOptions(depts)
} catch (error) {
- console.error('鎻愪氦澶辫触:', error)
+ console.error('鍔犺浇鐢ㄦ埛椤靛瓧鍏稿け璐�', error)
}
}
- const handleSelectionChange = (selection) => {
- selectedRows.value = selection
- console.log('閫変腑琛屾暟鎹�:', selectedRows.value)
+
+ onMounted(() => {
+ loadLookups()
+ })
+
+ const handleSearch = (params) => {
+ replaceSearchParams(buildUserSearchParams(params))
+ getData()
+ }
+
+ const handleReset = () => {
+ Object.assign(searchForm.value, createUserSearchState())
+ resetSearchParams()
+ }
+
+ const showDialog = (type, row) => {
+ dialogType.value = type
+ currentUserData.value = type === 'edit' ? buildUserDialogModel(row) : buildUserDialogModel()
+ dialogVisible.value = true
+ }
+
+ const loadUserDetail = async (id) => {
+ detailLoading.value = true
+ try {
+ return await fetchGetUserDetail(id)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ const openEditDialog = async (row) => {
+ try {
+ const detail = await loadUserDetail(row.id)
+ currentUserData.value = buildUserDialogModel(mergeUserDetailRecord(detail, row))
+ dialogType.value = 'edit'
+ dialogVisible.value = true
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鐢ㄦ埛璇︽儏澶辫触')
+ }
+ }
+
+ const openDetail = async (row) => {
+ detailDrawerVisible.value = true
+ try {
+ detailUserData.value = mergeUserDetailRecord(await loadUserDetail(row.id), row)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailUserData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鐢ㄦ埛璇︽儏澶辫触')
+ }
+ }
+
+ const handleDialogSubmit = async (formData) => {
+ const payload = buildUserSavePayload(formData)
+ try {
+ if (dialogType.value === 'edit') {
+ await fetchUpdateUser(payload)
+ ElMessage.success('淇敼鎴愬姛')
+ dialogVisible.value = false
+ currentUserData.value = buildUserDialogModel()
+ await refreshUpdate()
+ return
+ }
+ await fetchSaveUser(payload)
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ currentUserData.value = buildUserDialogModel()
+ await refreshCreate()
+ } catch (error) {
+ ElMessage.error(error?.message || '鎻愪氦澶辫触')
+ }
+ }
+
+ const handleDelete = async (row) => {
+ try {
+ 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 handleResetPassword = async (row) => {
+ try {
+ await ElMessageBox.confirm(
+ `纭畾灏嗙敤鎴枫��${row.username || row.nickname || row.id}銆嶇殑瀵嗙爜閲嶇疆涓� ${RESET_PASSWORD} 鍚楋紵`,
+ '閲嶇疆瀵嗙爜',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+ await fetchResetUserPassword({
+ id: row.id,
+ password: RESET_PASSWORD
+ })
+ ElMessage.success(`瀵嗙爜宸查噸缃负 ${RESET_PASSWORD}`)
+ await refreshUpdate()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '閲嶇疆瀵嗙爜澶辫触')
+ }
+ }
+ }
+
+ const handleStatusChange = async (row, checked) => {
+ const previousStatus = row.status
+ const previousStatusBool = row.statusBool
+ const nextStatus = checked ? 1 : 0
+ row._statusLoading = true
+ row.status = nextStatus
+ row.statusBool = checked
+
+ try {
+ await fetchUpdateUserStatus({
+ id: row.id,
+ status: nextStatus
+ })
+ ElMessage.success('鐘舵�佸凡鏇存柊')
+ await refreshUpdate()
+ } catch (error) {
+ row.status = previousStatus
+ row.statusBool = previousStatusBool
+ ElMessage.error(error?.message || '鐘舵�佹洿鏂板け璐�')
+ } finally {
+ row._statusLoading = 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
new file mode 100644
index 0000000..9bf4210
--- /dev/null
+++ b/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
@@ -0,0 +1,56 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐢ㄦ埛璇︽儏"
+ size="520px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="鐢ㄦ埛鍚�">{{ displayData.username || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄电О">{{ displayData.nickname || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閮ㄩ棬">{{ displayData.deptLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙掕壊">{{ displayData.roleNames || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ statusLabel }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵嬫満鍙�">{{ displayData.phone || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閭">{{ displayData.email || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐪熷疄濮撳悕">{{ displayData.realName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="韬唤璇佸彿">{{ displayData.idCard || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ彿">{{ displayData.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎬у埆">{{ sexLabel }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { getUserStatusMeta, normalizeUserListRow } from '../userPage.helpers'
+
+ const props = defineProps({
+ visible: { required: false, default: false },
+ loading: { required: false, default: false },
+ userData: { required: false, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const displayData = computed(() => normalizeUserListRow(props.userData))
+ const statusLabel = computed(() => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text)
+ const sexLabel = computed(() => {
+ switch (displayData.value.sex) {
+ case 1:
+ return '鐢�'
+ case 2:
+ return '濂�'
+ default:
+ return '鏈煡'
+ }
+ })
+
+ const handleVisibleChange = (visible) => {
+ emit('update:visible', visible)
+ }
+</script>
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 706c5c3..72725a3 100644
--- a/rsf-design/src/views/system/user/modules/user-dialog.vue
+++ b/rsf-design/src/views/system/user/modules/user-dialog.vue
@@ -1,106 +1,301 @@
<template>
<ElDialog
- v-model="dialogVisible"
- :title="dialogType === 'add' ? '娣诲姞鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
- width="30%"
+ :title="dialogTitle"
+ :model-value="visible"
+ @update:model-value="handleCancel"
+ width="960px"
align-center
+ class="user-dialog"
+ @closed="handleClosed"
>
- <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
- <ElFormItem label="鐢ㄦ埛鍚�" prop="username">
- <ElInput v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
- </ElFormItem>
- <ElFormItem label="鎵嬫満鍙�" prop="phone">
- <ElInput v-model="formData.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
- </ElFormItem>
- <ElFormItem label="鎬у埆" prop="gender">
- <ElSelect v-model="formData.gender">
- <ElOption label="鐢�" value="鐢�" />
- <ElOption label="濂�" value="濂�" />
- </ElSelect>
- </ElFormItem>
- <ElFormItem label="瑙掕壊" prop="role">
- <ElSelect v-model="formData.role" multiple>
- <ElOption
- v-for="role in roleList"
- :key="role.roleCode"
- :value="role.roleCode"
- :label="role.roleName"
- />
- </ElSelect>
- </ElFormItem>
- </ElForm>
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
<template #footer>
- <div class="dialog-footer">
- <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
- <ElButton type="primary" @click="handleSubmit">鎻愪氦</ElButton>
- </div>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
</template>
</ElDialog>
</template>
<script setup>
- import { ROLE_LIST_DATA } from '@/mock/temp/formData'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildUserDialogModel, createUserFormState } from '../userPage.helpers'
+
const props = defineProps({
- visible: { required: true },
- type: { required: true },
- userData: { required: false }
+ visible: { required: false, default: false },
+ type: { required: false, default: 'add' },
+ userData: { required: false, default: () => ({}) },
+ roleOptions: { required: false, default: () => [] },
+ deptTreeOptions: { required: false, default: () => [] }
})
+
const emit = defineEmits(['update:visible', 'submit'])
- const roleList = ref(ROLE_LIST_DATA)
- const dialogVisible = computed({
- get: () => props.visible,
- set: (value) => emit('update:visible', value)
- })
- const dialogType = computed(() => props.type)
const formRef = ref()
- const formData = reactive({
- username: '',
- phone: '',
- gender: '鐢�',
- role: []
- })
- const rules = {
- username: [
- { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' },
- { min: 2, max: 20, message: '闀垮害鍦� 2 鍒� 20 涓瓧绗�', trigger: 'blur' }
- ],
- phone: [
- { required: true, message: '璇疯緭鍏ユ墜鏈哄彿', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙锋牸寮�', trigger: 'blur' }
- ],
- gender: [{ required: true, message: '璇烽�夋嫨鎬у埆', trigger: 'blur' }],
- role: [{ required: true, message: '璇烽�夋嫨瑙掕壊', trigger: 'blur' }]
+ const form = reactive(createUserFormState())
+
+ const isEdit = computed(() => props.type === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鐢ㄦ埛' : '鏂板鐢ㄦ埛'))
+
+ const validatePassword = (_rule, value, callback) => {
+ if (!isEdit.value && !value) {
+ callback(new Error('璇疯緭鍏ュ瘑鐮�'))
+ return
+ }
+ if (value && String(value).length < 6) {
+ callback(new Error('瀵嗙爜闀垮害鑷冲皯 6 浣�'))
+ return
+ }
+ callback()
}
- const initFormData = () => {
- const isEdit = props.type === 'edit' && props.userData
- const row = props.userData
- Object.assign(formData, {
- username: isEdit && row ? row.userName || '' : '',
- phone: isEdit && row ? row.userPhone || '' : '',
- gender: isEdit && row ? row.userGender || '鐢�' : '鐢�',
- role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
- })
+
+ const validateConfirmPassword = (_rule, value, callback) => {
+ if (!form.password && !value && isEdit.value) {
+ callback()
+ return
+ }
+ if (!value) {
+ callback(new Error('璇峰啀娆¤緭鍏ュ瘑鐮�'))
+ return
+ }
+ if (value !== form.password) {
+ callback(new Error('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�'))
+ return
+ }
+ callback()
}
+
+ const rules = computed(() => ({
+ username: [{ required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }],
+ nickname: [{ required: true, message: '璇疯緭鍏ユ樀绉�', trigger: 'blur' }],
+ roleIds: [{ type: 'array', required: true, message: '璇烽�夋嫨瑙掕壊', trigger: 'change' }],
+ password: [{ validator: validatePassword, trigger: 'blur' }],
+ confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鐢ㄦ埛鍚�',
+ key: 'username',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕',
+ clearable: true
+ }
+ },
+ {
+ label: '鏄电О',
+ key: 'nickname',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ樀绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '閮ㄩ棬',
+ key: 'deptId',
+ type: 'treeselect',
+ props: {
+ data: props.deptTreeOptions,
+ props: {
+ label: 'label',
+ value: 'value',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨閮ㄩ棬',
+ clearable: true,
+ checkStrictly: true
+ }
+ },
+ {
+ label: '瑙掕壊',
+ key: 'roleIds',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨瑙掕壊',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.roleOptions
+ }
+ },
+ {
+ label: '瀵嗙爜',
+ key: 'password',
+ type: 'input',
+ props: {
+ type: 'password',
+ showPassword: true,
+ placeholder: isEdit.value ? '涓嶄慨鏀瑰垯鐣欑┖' : '璇疯緭鍏ュ瘑鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '纭瀵嗙爜',
+ key: 'confirmPassword',
+ type: 'input',
+ props: {
+ type: 'password',
+ showPassword: true,
+ placeholder: '璇峰啀娆¤緭鍏ュ瘑鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鎬у埆',
+ key: 'sex',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鎬у埆',
+ clearable: true,
+ options: [
+ { label: '鏈煡', value: 0 },
+ { label: '鐢�', value: 1 },
+ { label: '濂�', value: 2 }
+ ]
+ }
+ },
+ {
+ label: '宸ュ彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ伐鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: '鎵嬫満鍙�',
+ key: 'phone',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ墜鏈哄彿',
+ clearable: true
+ }
+ },
+ {
+ label: '閭',
+ key: 'email',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ偖绠�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐪熷疄濮撳悕',
+ key: 'realName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ湡瀹炲鍚�',
+ clearable: true
+ }
+ },
+ {
+ label: '韬唤璇佸彿',
+ key: 'idCard',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ韩浠借瘉鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ },
+ span: 24
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createUserFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildUserDialogModel(props.userData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
watch(
- () => [props.visible, props.type, props.userData],
- ([visible]) => {
+ () => props.visible,
+ (visible) => {
if (visible) {
- initFormData()
+ loadFormData()
nextTick(() => {
- formRef.value?.clearValidate()
+ formRef.value?.clearValidate?.()
})
}
},
{ immediate: true }
)
- const handleSubmit = async () => {
- if (!formRef.value) return
- await formRef.value.validate((valid) => {
- if (valid) {
- ElMessage.success(dialogType.value === 'add' ? '娣诲姞鎴愬姛' : '鏇存柊鎴愬姛')
- dialogVisible.value = false
- emit('submit')
+
+ watch(
+ () => props.userData,
+ () => {
+ if (props.visible) {
+ loadFormData()
}
- })
- }
+ },
+ { deep: true }
+ )
+
+ watch(
+ () => props.type,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ }
+ )
</script>
diff --git a/rsf-design/tests/auth-contract.test.mjs b/rsf-design/tests/auth-contract.test.mjs
new file mode 100644
index 0000000..6dd0d4e
--- /dev/null
+++ b/rsf-design/tests/auth-contract.test.mjs
@@ -0,0 +1,123 @@
+import assert from 'node:assert/strict'
+import { register } from 'node:module'
+import test from 'node:test'
+
+register(
+ 'data:text/javascript,export function resolve(specifier, context, nextResolve){ if(specifier===\'@/utils/http\'){ return { shortCircuit:true, url:\'data:text/javascript,export default { post(options){ globalThis.__authHttpCalls.push({ method:"post", options }); return Promise.resolve({ accessToken:"abc", refreshToken:"ref", user:{ id:1 } }) }, get(options){ globalThis.__authHttpCalls.push({ method:"get", options }); return Promise.resolve({ userId:1, roles:[{ code:"R_ADMIN", name:"绠$悊鍛�" }], authorities:[{ authority:"system:menu:save" },{ authority:"system:menu:update" }] }) } }\' } } return nextResolve(specifier, context) }',
+ import.meta.url
+)
+
+globalThis.__authHttpCalls = []
+
+const {
+ buildLoginPayload,
+ fetchGetUserInfo,
+ fetchLogin,
+ normalizeLoginResponse,
+ normalizeUserInfo
+} = await import('../src/api/auth.js')
+
+test('buildLoginPayload matches the rsf-server login contract', () => {
+ assert.deepEqual(buildLoginPayload({ username: 'demo', password: '123456' }), {
+ username: 'demo',
+ password: '123456'
+ })
+})
+
+test('normalizeLoginResponse extracts the real token fields', () => {
+ assert.deepEqual(
+ normalizeLoginResponse({
+ code: 200,
+ data: { accessToken: 'abc', user: { id: 1 } }
+ }),
+ { accessToken: 'abc', refreshToken: '', user: { id: 1 } }
+ )
+})
+
+test('normalizeLoginResponse also handles an inner data object', () => {
+ assert.deepEqual(
+ normalizeLoginResponse({
+ accessToken: 'abc',
+ refreshToken: 'ref',
+ user: { id: 1 }
+ }),
+ { accessToken: 'abc', refreshToken: 'ref', user: { id: 1 } }
+ )
+})
+
+test('normalizeLoginResponse accepts the old backend accessToken shape', () => {
+ const result = normalizeLoginResponse({
+ code: 200,
+ data: { accessToken: 'token-1', refreshToken: '', user: { username: 'admin' } }
+ })
+
+ assert.equal(result.accessToken, 'token-1')
+})
+
+test('normalizeLoginResponse keeps missing accessToken empty', () => {
+ const result = normalizeLoginResponse({
+ code: 200,
+ data: { refreshToken: 'ref', user: { username: 'admin' } }
+ })
+
+ assert.equal(result.accessToken, '')
+})
+
+test('normalizeUserInfo returns roles and buttons arrays safely', () => {
+ assert.deepEqual(normalizeUserInfo({ userId: 1, roles: ['R_ADMIN'], buttons: ['add'] }), {
+ userId: 1,
+ roles: ['R_ADMIN'],
+ buttons: ['add']
+ })
+})
+
+test('normalizeUserInfo derives role codes and button aliases from rsf-server auth payloads', () => {
+ assert.deepEqual(
+ normalizeUserInfo({
+ userId: 1,
+ roles: [{ code: 'R_SUPER', name: '瓒呯骇绠$悊鍛�' }],
+ authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }]
+ }),
+ {
+ userId: 1,
+ roles: ['R_SUPER'],
+ authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
+ buttons: ['system:menu:save', 'system:menu:update']
+ }
+ )
+})
+
+test('fetchLogin keeps supporting legacy userName input at the boundary', async () => {
+ const result = await fetchLogin({ userName: 'demo', password: '123456' })
+
+ assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
+ method: 'post',
+ options: {
+ url: '/login',
+ params: { username: 'demo', password: '123456' }
+ }
+ })
+ assert.deepEqual(result, {
+ token: 'abc',
+ accessToken: 'abc',
+ refreshToken: 'ref',
+ user: { id: 1 }
+ })
+})
+
+test('fetchGetUserInfo normalizes the returned user payload', async () => {
+ const result = await fetchGetUserInfo()
+
+ assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
+ method: 'get',
+ options: {
+ url: '/auth/user'
+ }
+ })
+ assert.deepEqual(result, {
+ userId: 1,
+ roles: ['R_ADMIN'],
+ authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
+ buttons: ['system:menu:save', 'system:menu:update']
+ })
+})
diff --git a/rsf-design/tests/list-export-print-contract.test.mjs b/rsf-design/tests/list-export-print-contract.test.mjs
new file mode 100644
index 0000000..d7a2f50
--- /dev/null
+++ b/rsf-design/tests/list-export-print-contract.test.mjs
@@ -0,0 +1,103 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import {
+ buildListExportPayload,
+ buildPrintPageQuery,
+ toExportColumns
+} from '../src/components/biz/list-export-print/list-export-print.helpers.js'
+
+test('selected rows keep export payload on ids only instead of flat query params', () => {
+ const payload = buildListExportPayload({
+ reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+ selectedRows: [{ id: 7 }, { id: 9 }],
+ queryParams: { current: 1, pageSize: 20, name: '绠$悊鍛�', code: 'R_ADMIN' },
+ columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
+ })
+
+ assert.deepEqual(payload.ids, [7, 9])
+ assert.deepEqual(payload.columns, [{ source: 'name', label: '瑙掕壊鍚嶇О' }])
+ assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+ assert.deepEqual(
+ Object.keys(payload).filter((key) => ['current', 'pageSize', 'name', 'code', 'queryParams'].includes(key)),
+ []
+ )
+})
+
+test('export columns use ListExportService source/label contract only', () => {
+ assert.deepEqual(
+ toExportColumns([
+ { prop: 'name', label: '瑙掕壊鍚嶇О' },
+ { prop: 'operation', label: '鎿嶄綔' },
+ { prop: 'selection', label: '鍕鹃��' }
+ ]),
+ [{ source: 'name', label: '瑙掕壊鍚嶇О' }]
+ )
+})
+
+test('export payload keeps no-filter searches legal as flat params', () => {
+ const payload = buildListExportPayload({
+ reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+ selectedRows: [],
+ queryParams: { current: 1, pageSize: 20 },
+ columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
+ })
+
+ assert.equal(payload.current, 1)
+ assert.equal(payload.pageSize, 20)
+ assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+ assert.deepEqual(payload.ids, [])
+ assert.equal(Object.prototype.hasOwnProperty.call(payload, 'queryParams'), false)
+})
+
+test('export payload preserves report style meta and column align without top-level reportStyle', () => {
+ const payload = buildListExportPayload({
+ reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+ meta: {
+ reportStyle: {
+ orientation: 'landscape',
+ density: 'comfortable'
+ }
+ },
+ columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О', align: 'right' }]
+ })
+
+ assert.deepEqual(payload.meta.reportStyle, {
+ orientation: 'landscape',
+ density: 'comfortable'
+ })
+ assert.equal(payload.columns[0].align, 'right')
+ assert.equal(Object.prototype.hasOwnProperty.call(payload, 'reportStyle'), false)
+})
+
+test('print query expands to the full result set instead of the current page size', () => {
+ assert.deepEqual(
+ buildPrintPageQuery({
+ queryParams: { current: 3, pageSize: 20, orderBy: 'createTime desc', name: '绠$悊鍛�' },
+ total: 86,
+ maxResults: 1000
+ }),
+ {
+ current: 1,
+ pageSize: 86,
+ orderBy: 'createTime desc',
+ name: '绠$悊鍛�'
+ }
+ )
+})
+
+test('print query caps pageSize at maxResults when total is larger', () => {
+ assert.deepEqual(
+ buildPrintPageQuery({
+ queryParams: { current: 5, pageSize: 20, orderBy: 'createTime desc', code: 'R_ADMIN' },
+ total: 1500,
+ maxResults: 1000
+ }),
+ {
+ current: 1,
+ pageSize: 1000,
+ orderBy: 'createTime desc',
+ code: 'R_ADMIN'
+ }
+ )
+})
diff --git a/rsf-design/tests/system-role-print-export-page.test.mjs b/rsf-design/tests/system-role-print-export-page.test.mjs
new file mode 100644
index 0000000..3b557c6
--- /dev/null
+++ b/rsf-design/tests/system-role-print-export-page.test.mjs
@@ -0,0 +1,97 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import test from 'node:test'
+
+import * as roleHelpers from '../src/views/system/role/rolePage.helpers.js'
+
+test('role report columns are the fixed business columns, not table utility columns', () => {
+ assert.deepEqual(
+ roleHelpers.getRoleReportColumns().map((column) => column.source),
+ ['name', 'code', 'statusText', 'memo', 'createTimeText', 'updateTimeText']
+ )
+})
+
+test('report columns keep visible order inside the role allowlist', () => {
+ assert.deepEqual(
+ roleHelpers.resolveRoleReportColumns([
+ { prop: 'selection', label: '鍕鹃��' },
+ { prop: 'status', label: '鐘舵��' },
+ { prop: 'name', label: '瑙掕壊鍚嶇О' },
+ { prop: 'deptName', label: '閮ㄩ棬鍚嶇О' },
+ { prop: 'operation', label: '鎿嶄綔' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]),
+ [
+ { source: 'statusText', label: '鐘舵��' },
+ { source: 'name', label: '瑙掕壊鍚嶇О' },
+ { source: 'memo', label: '澶囨敞' }
+ ]
+ )
+})
+
+test('role print rows expose formatted status text', () => {
+ const rows = roleHelpers.buildRolePrintRows([
+ { name: '绠$悊鍛�', status: 1 },
+ { name: '璁垮', status: 0 }
+ ])
+
+ assert.equal(rows[0].statusText, '姝e父')
+ assert.equal(rows[1].statusText, '绂佺敤')
+})
+
+test('role report meta applies shared report style and title', () => {
+ const meta = roleHelpers.buildRoleReportMeta({
+ previewMeta: {
+ reportDate: '2026-03-29',
+ printedAt: '2026-03-29 10:57:25',
+ operator: 'ROOT'
+ },
+ count: 2,
+ titleAlign: 'left',
+ titleLevel: 'prominent'
+ })
+
+ assert.equal(meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+ assert.equal(meta.reportDate, '2026-03-29')
+ assert.equal(meta.operator, 'ROOT')
+ assert.deepEqual(meta.reportStyle, {
+ titleAlign: 'left',
+ titleLevel: 'prominent',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+ })
+})
+
+test('role page uses helper report defaults as single source of truth', () => {
+ const indexSource = readFileSync(new URL('../src/views/system/role/index.vue', import.meta.url), 'utf8')
+ const meta = roleHelpers.buildRoleReportMeta()
+
+ assert.equal(roleHelpers.ROLE_REPORT_TITLE, '瑙掕壊绠$悊鎶ヨ〃')
+ assert.deepEqual(roleHelpers.ROLE_REPORT_STYLE, {
+ titleAlign: 'center',
+ titleLevel: 'strong'
+ })
+ assert.equal(meta.reportTitle, roleHelpers.ROLE_REPORT_TITLE)
+ assert.deepEqual(meta.reportStyle, {
+ ...roleHelpers.ROLE_REPORT_STYLE,
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+ })
+ assert.match(
+ indexSource,
+ /import\s*\{[\s\S]*\bROLE_REPORT_STYLE\b,[\s\S]*\bROLE_REPORT_TITLE\b[\s\S]*\}\s*from '\.\/rolePage\.helpers'/
+ )
+ assert.match(
+ indexSource,
+ /<ListExportPrint[\s\S]*:report-title="reportTitle"[\s\S]*:preview-meta="resolvedPreviewMeta"[\s\S]*\/>/
+ )
+ assert.match(indexSource, /const reportTitle = ROLE_REPORT_TITLE/)
+ assert.doesNotMatch(indexSource, /const reportTitle = '瑙掕壊绠$悊鎶ヨ〃'/)
+ assert.doesNotMatch(indexSource, /const ROLE_REPORT_STYLE = \{/)
+ assert.match(
+ indexSource,
+ /const resolvedPreviewMeta = computed\(\(\) =>\s*buildRoleReportMeta\(\{\s*previewMeta: previewMeta\.value,\s*count: previewRows\.value\.length,\s*titleAlign: ROLE_REPORT_STYLE\.titleAlign,\s*titleLevel: ROLE_REPORT_STYLE\.titleLevel\s*\}\)\s*\)/
+ )
+})
--
Gitblit v1.9.1