feat: wire all management pages to backend (config/dict/org/resource/role/audit/login-log)
- 401 自动登出:http 会话过期回调 + main.ts 注册(清 menu/tabs + 跳登录) - 参数配置/字典/组织/资源/角色/操作日志/登录日志对接真实接口 - 角色管理新增权限分配树(el-tree 勾选资源,/roles/:code/resources) - 操作日志/登录日志只读页(ty-search-form + ty-table + useRequest) - listOrgs 迁到 api/org.ts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
77917573c1
commit
f1cb5723b2
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -44,6 +44,7 @@ declare module 'vue' {
|
|||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
@ -101,6 +102,7 @@ declare global {
|
|||||||
const ElTable: typeof import('element-plus/es')['ElTable']
|
const ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
const ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
const ElTag: typeof import('element-plus/es')['ElTag']
|
const ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
const ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
const Icon: typeof import('./src/components/icon/icon.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
34
src/api/audit-log.ts
Normal file
34
src/api/audit-log.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
import type { PageResult } from '@/types/crud'
|
||||||
|
|
||||||
|
export interface AuditLog {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
userId: string | null
|
||||||
|
username: string | null
|
||||||
|
module: string | null
|
||||||
|
action: string | null
|
||||||
|
method: string | null
|
||||||
|
path: string | null
|
||||||
|
status: string | null
|
||||||
|
ip: string | null
|
||||||
|
userAgent: string | null
|
||||||
|
durationMs: number | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
username?: string
|
||||||
|
module?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAuditLogs(query: AuditLogQuery, signal?: AbortSignal): Promise<PageResult<AuditLog>> {
|
||||||
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
|
if (query.username) params.username = query.username
|
||||||
|
if (query.module) params.module = query.module
|
||||||
|
if (query.status) params.status = query.status
|
||||||
|
return http.get<PageResult<AuditLog>>('/audit-logs', { params, signal })
|
||||||
|
}
|
||||||
54
src/api/config.ts
Normal file
54
src/api/config.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
key: string
|
||||||
|
value: string | null
|
||||||
|
remark: string | null
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigListQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
key?: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigListResult {
|
||||||
|
list: Config[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigUpsertData {
|
||||||
|
key?: string
|
||||||
|
value?: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置分页列表。 */
|
||||||
|
export function listConfigs(query: ConfigListQuery, signal?: AbortSignal): Promise<ConfigListResult> {
|
||||||
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
|
if (query.key) params.key = query.key
|
||||||
|
if (query.value) params.value = query.value
|
||||||
|
return http.get<ConfigListResult>('/configs', { params, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增配置。 */
|
||||||
|
export function createConfig(data: ConfigUpsertData): Promise<Config> {
|
||||||
|
return http.post<Config>('/configs', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新配置(key 不可改)。 */
|
||||||
|
export function updateConfig(id: string, data: ConfigUpsertData): Promise<Config> {
|
||||||
|
return http.patch<Config>(`/configs/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除配置(软删)。 */
|
||||||
|
export function deleteConfig(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/configs/${id}`)
|
||||||
|
}
|
||||||
74
src/api/dict.ts
Normal file
74
src/api/dict.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export interface DictType {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
status: 'enabled' | 'disabled'
|
||||||
|
remark: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DictItem {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
dictTypeCode: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
sort: number
|
||||||
|
status: 'enabled' | 'disabled'
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 字典类型 ----
|
||||||
|
|
||||||
|
export function listDictTypes(page = 1, pageSize = 1000, signal?: AbortSignal): Promise<PageResult<DictType>> {
|
||||||
|
return http.get<PageResult<DictType>>('/dict-types', { params: { page, pageSize }, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDictType(data: { code: string; name: string; remark?: string }): Promise<DictType> {
|
||||||
|
return http.post<DictType>('/dict-types', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDictType(id: string, data: { name?: string; status?: string; remark?: string }): Promise<DictType> {
|
||||||
|
return http.patch<DictType>(`/dict-types/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDictType(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/dict-types/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 字典项 ----
|
||||||
|
|
||||||
|
/** 某字典类型下的全部字典项(后端不分页,按 sort 升序返回)。 */
|
||||||
|
export function listDictItems(dictTypeCode: string, signal?: AbortSignal): Promise<DictItem[]> {
|
||||||
|
return http.get<DictItem[]>('/dict-items', { params: { dictTypeCode }, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDictItem(data: {
|
||||||
|
dictTypeCode: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
sort?: number
|
||||||
|
}): Promise<DictItem> {
|
||||||
|
return http.post<DictItem>('/dict-items', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDictItem(
|
||||||
|
id: string,
|
||||||
|
data: { label?: string; value?: string; sort?: number; status?: string },
|
||||||
|
): Promise<DictItem> {
|
||||||
|
return http.patch<DictItem>(`/dict-items/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDictItem(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/dict-items/${id}`)
|
||||||
|
}
|
||||||
@ -19,6 +19,13 @@ export class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 会话过期回调(token 刷新失败时触发),由应用启动时注册。 */
|
||||||
|
let onAuthExpired: (() => void) | null = null
|
||||||
|
|
||||||
|
export function setAuthExpiredHandler(handler: () => void): void {
|
||||||
|
onAuthExpired = handler
|
||||||
|
}
|
||||||
|
|
||||||
interface HttpClientOptions {
|
interface HttpClientOptions {
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
timeout?: number
|
timeout?: number
|
||||||
@ -68,6 +75,7 @@ export class HttpClient {
|
|||||||
return this.instance.request(config)
|
return this.instance.request(config)
|
||||||
} catch {
|
} catch {
|
||||||
clearTokens()
|
clearTokens()
|
||||||
|
onAuthExpired?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const body = error.response?.data as ApiEnvelope | undefined
|
const body = error.response?.data as ApiEnvelope | undefined
|
||||||
|
|||||||
27
src/api/login-log.ts
Normal file
27
src/api/login-log.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
import type { PageResult } from '@/types/crud'
|
||||||
|
|
||||||
|
export interface LoginLog {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
username: string | null
|
||||||
|
ip: string | null
|
||||||
|
userAgent: string | null
|
||||||
|
status: string
|
||||||
|
message: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginLogQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
username?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listLoginLogs(query: LoginLogQuery, signal?: AbortSignal): Promise<PageResult<LoginLog>> {
|
||||||
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
|
if (query.username) params.username = query.username
|
||||||
|
if (query.status) params.status = query.status
|
||||||
|
return http.get<PageResult<LoginLog>>('/login-logs', { params, signal })
|
||||||
|
}
|
||||||
39
src/api/org.ts
Normal file
39
src/api/org.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export interface Org {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
parentCode: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
leader: string | null
|
||||||
|
sort: number
|
||||||
|
status: 'enabled' | 'disabled'
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组织列表(后端按 sort 升序返回普通数组,前端自行组树)。 */
|
||||||
|
export function listOrgs(signal?: AbortSignal): Promise<Org[]> {
|
||||||
|
return http.get<Org[]>('/orgs', { signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOrg(data: {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
parentCode?: string | null
|
||||||
|
leader?: string
|
||||||
|
sort?: number
|
||||||
|
}): Promise<Org> {
|
||||||
|
return http.post<Org>('/orgs', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOrg(
|
||||||
|
id: string,
|
||||||
|
data: { name?: string; parentCode?: string | null; leader?: string; sort?: number; status?: string },
|
||||||
|
): Promise<Org> {
|
||||||
|
return http.patch<Org>(`/orgs/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteOrg(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/orgs/${id}`)
|
||||||
|
}
|
||||||
51
src/api/resource.ts
Normal file
51
src/api/resource.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export type ResourceType = 'menu' | 'button' | 'api'
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
parentCode: string | null
|
||||||
|
type: ResourceType
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
path: string | null
|
||||||
|
component: string | null
|
||||||
|
method: string | null
|
||||||
|
apiPath: string | null
|
||||||
|
icon: string | null
|
||||||
|
sort: number
|
||||||
|
status: 'enabled' | 'disabled'
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceUpsertData {
|
||||||
|
type?: ResourceType
|
||||||
|
code?: string
|
||||||
|
name?: string
|
||||||
|
parentCode?: string | null
|
||||||
|
path?: string
|
||||||
|
component?: string
|
||||||
|
method?: string
|
||||||
|
apiPath?: string
|
||||||
|
icon?: string
|
||||||
|
sort?: number
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 资源列表(后端按 sort 升序返回普通数组,前端自行组树)。 */
|
||||||
|
export function listResources(signal?: AbortSignal): Promise<Resource[]> {
|
||||||
|
return http.get<Resource[]>('/resources', { signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResource(data: ResourceUpsertData): Promise<Resource> {
|
||||||
|
return http.post<Resource>('/resources', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateResource(id: string, data: ResourceUpsertData): Promise<Resource> {
|
||||||
|
return http.patch<Resource>(`/resources/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteResource(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/resources/${id}`)
|
||||||
|
}
|
||||||
62
src/api/role.ts
Normal file
62
src/api/role.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export type DataScope = 'all' | 'dept' | 'dept_and_child' | 'self' | 'custom'
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
dataScope: DataScope
|
||||||
|
status: 'enabled' | 'disabled'
|
||||||
|
remark: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleListQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
name?: string
|
||||||
|
code?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleListResult {
|
||||||
|
list: Role[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listRoles(query: RoleListQuery, signal?: AbortSignal): Promise<RoleListResult> {
|
||||||
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
|
if (query.name) params.name = query.name
|
||||||
|
if (query.code) params.code = query.code
|
||||||
|
if (query.status) params.status = query.status
|
||||||
|
return http.get<RoleListResult>('/roles', { params, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRole(data: { code: string; name: string; dataScope?: DataScope; remark?: string }): Promise<Role> {
|
||||||
|
return http.post<Role>('/roles', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRole(
|
||||||
|
id: string,
|
||||||
|
data: { name?: string; dataScope?: DataScope; status?: string; remark?: string },
|
||||||
|
): Promise<Role> {
|
||||||
|
return http.patch<Role>(`/roles/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteRole(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/roles/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 角色已分配的资源 code 列表(注意:URL 的 :id 是 roleCode 而非 UUID)。 */
|
||||||
|
export function listAssignedResources(roleCode: string, signal?: AbortSignal): Promise<string[]> {
|
||||||
|
return http.get<string[]>(`/roles/${roleCode}/resources`, { signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配资源(注意:URL 的 :id 是 roleCode 而非 UUID)。 */
|
||||||
|
export function assignResources(roleCode: string, resourceCodes: string[]): Promise<unknown> {
|
||||||
|
return http.put(`/roles/${roleCode}/resources`, { resourceCodes })
|
||||||
|
}
|
||||||
@ -42,12 +42,6 @@ export interface UserUpsertData {
|
|||||||
status?: UserStatus
|
status?: UserStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Org {
|
|
||||||
id: string
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 用户分页列表。 */
|
/** 用户分页列表。 */
|
||||||
export function listUsers(query: UserListQuery, signal?: AbortSignal): Promise<UserListResult> {
|
export function listUsers(query: UserListQuery, signal?: AbortSignal): Promise<UserListResult> {
|
||||||
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
@ -77,8 +71,3 @@ export function deleteUser(id: string): Promise<unknown> {
|
|||||||
export function resetPassword(id: string, password: string): Promise<unknown> {
|
export function resetPassword(id: string, password: string): Promise<unknown> {
|
||||||
return http.patch(`/users/${id}/password`, { password })
|
return http.patch(`/users/${id}/password`, { password })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组织列表(用于组织下拉)。 */
|
|
||||||
export function listOrgs(signal?: AbortSignal): Promise<Org[]> {
|
|
||||||
return http.get<Org[]>('/orgs', { signal })
|
|
||||||
}
|
|
||||||
|
|||||||
17
src/main.ts
17
src/main.ts
@ -2,16 +2,29 @@ import { createApp } from 'vue'
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { installRouter } from './router'
|
import router, { installRouter } from './router'
|
||||||
import { installStore } from '@/stores'
|
import pinia, { installStore } from '@/stores'
|
||||||
import { installAssets } from '@/plugins/assets'
|
import { installAssets } from '@/plugins/assets'
|
||||||
import { Env } from '@/utils/env.ts'
|
import { Env } from '@/utils/env.ts'
|
||||||
import { installI18n } from '@/i18n'
|
import { installI18n } from '@/i18n'
|
||||||
|
import { setAuthExpiredHandler } from '@/api/http'
|
||||||
|
import { useMenuStore } from '@/stores/modules/menu'
|
||||||
|
import { useTabsStore } from '@/stores/modules/tabs'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
installRouter(app)
|
installRouter(app)
|
||||||
installStore(app)
|
installStore(app)
|
||||||
|
|
||||||
|
// 会话过期(401 刷新失败)时:清状态 + 跳登录
|
||||||
|
setAuthExpiredHandler(() => {
|
||||||
|
useMenuStore(pinia).reset()
|
||||||
|
useTabsStore(pinia).removeAll()
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
installI18n(app)
|
installI18n(app)
|
||||||
installAssets()
|
installAssets()
|
||||||
|
|
||||||
|
|||||||
@ -112,6 +112,7 @@ import { useAuthStore } from '@/stores/modules/auth'
|
|||||||
import { useAppStore } from '@/stores/modules/app'
|
import { useAppStore } from '@/stores/modules/app'
|
||||||
import { defaultLoginConfig } from '@/config/login'
|
import { defaultLoginConfig } from '@/config/login'
|
||||||
import { applyTheme } from '@/utils/theme'
|
import { applyTheme } from '@/utils/theme'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -134,7 +135,6 @@ if (qUsername) model.username = String(qUsername)
|
|||||||
const captcha = ref('')
|
const captcha = ref('')
|
||||||
const captchaCode = ref('')
|
const captchaCode = ref('')
|
||||||
const remember = ref(true)
|
const remember = ref(true)
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
const rootStyle = computed(() => (config.background ? { background: config.background } : {}))
|
const rootStyle = computed(() => (config.background ? { background: config.background } : {}))
|
||||||
@ -172,6 +172,19 @@ function refreshCaptcha(): void {
|
|||||||
}
|
}
|
||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
|
|
||||||
|
const { loading, run: doLogin } = useRequest(
|
||||||
|
() => auth.login(config.tenantCode, model.username, model.password),
|
||||||
|
{
|
||||||
|
manual: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push(config.homePath)
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
error.value = (e as Error).message
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async function onSubmit(): Promise<void> {
|
async function onSubmit(): Promise<void> {
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@ -186,15 +199,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
captcha.value = ''
|
captcha.value = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = true
|
await doLogin()
|
||||||
try {
|
|
||||||
await auth.login(config.tenantCode, model.username, model.password)
|
|
||||||
router.push(config.homePath)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = (e as Error).message
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onForget(): void {
|
function onForget(): void {
|
||||||
|
|||||||
115
src/pages/monitor/audit/index.vue
Normal file
115
src/pages/monitor/audit/index.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-page">
|
||||||
|
<el-card class="log-page__search" shadow="never">
|
||||||
|
<ty-search-form :schema="searchSchema" :model-value="searchQuery" @search="handleSearch" @reset="handleReset" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="log-page__table" shadow="never">
|
||||||
|
<div class="log-page__table-body">
|
||||||
|
<ty-table
|
||||||
|
:columns="columns"
|
||||||
|
:data="list"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import type { JsonSchema, TableColumn } from '@/types/crud'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
|
import { listAuditLogs } from '@/api/audit-log'
|
||||||
|
|
||||||
|
const STATUS_MAP = {
|
||||||
|
success: { label: '成功', type: 'success' },
|
||||||
|
fail: { label: '失败', type: 'danger' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const columns: TableColumn[] = [
|
||||||
|
{ prop: 'username', label: '操作人', width: 100 },
|
||||||
|
{ prop: 'module', label: '模块', width: 100 },
|
||||||
|
{ prop: 'action', label: '动作', width: 100 },
|
||||||
|
{ prop: 'method', label: '方法', width: 80, align: 'center' },
|
||||||
|
{ prop: 'path', label: '路径', minWidth: 180 },
|
||||||
|
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||||
|
{ prop: 'ip', label: 'IP', width: 130 },
|
||||||
|
{ prop: 'durationMs', label: '耗时(ms)', width: 90, align: 'center' },
|
||||||
|
{ prop: 'createdAt', label: '时间', width: 160, type: 'datetime' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
username: { type: 'string', title: '操作人', placeholder: '请输入操作人' },
|
||||||
|
module: { type: 'string', title: '模块', placeholder: '请输入模块' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
title: '状态',
|
||||||
|
enum: ['success', 'fail'],
|
||||||
|
enumNames: ['成功', '失败'],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const searchQuery = reactive({ username: '', module: '', status: '' })
|
||||||
|
const appliedQuery = reactive({ username: '', module: '', status: '' })
|
||||||
|
|
||||||
|
const { data, loading, run } = useRequest(
|
||||||
|
(signal: AbortSignal) => listAuditLogs({ page: page.value, pageSize: pageSize.value, ...appliedQuery }, signal),
|
||||||
|
{ manual: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const list = computed(() => data.value?.list ?? [])
|
||||||
|
const total = computed(() => data.value?.total ?? 0)
|
||||||
|
|
||||||
|
function handleSearch(): void {
|
||||||
|
appliedQuery.username = searchQuery.username
|
||||||
|
appliedQuery.module = searchQuery.module
|
||||||
|
appliedQuery.status = searchQuery.status
|
||||||
|
page.value = 1
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset(): void {
|
||||||
|
appliedQuery.username = ''
|
||||||
|
appliedQuery.module = ''
|
||||||
|
appliedQuery.status = ''
|
||||||
|
page.value = 1
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(run)
|
||||||
|
watch([page, pageSize], () => {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.log-page {
|
||||||
|
@apply flex h-full flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__search {
|
||||||
|
@apply mb-md flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__table {
|
||||||
|
@apply flex min-h-0 flex-1 flex-col;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
@apply flex min-h-0 flex-1 flex-col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__table-body {
|
||||||
|
@apply min-h-0 flex-1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
src/pages/monitor/login-log/index.vue
Normal file
109
src/pages/monitor/login-log/index.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-page">
|
||||||
|
<el-card class="log-page__search" shadow="never">
|
||||||
|
<ty-search-form :schema="searchSchema" :model-value="searchQuery" @search="handleSearch" @reset="handleReset" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="log-page__table" shadow="never">
|
||||||
|
<div class="log-page__table-body">
|
||||||
|
<ty-table
|
||||||
|
:columns="columns"
|
||||||
|
:data="list"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
v-model:page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import type { JsonSchema, TableColumn } from '@/types/crud'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
|
import { listLoginLogs } from '@/api/login-log'
|
||||||
|
|
||||||
|
const STATUS_MAP = {
|
||||||
|
success: { label: '成功', type: 'success' },
|
||||||
|
fail: { label: '失败', type: 'danger' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const columns: TableColumn[] = [
|
||||||
|
{ prop: 'username', label: '用户名', width: 120 },
|
||||||
|
{ prop: 'ip', label: 'IP', width: 130 },
|
||||||
|
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||||
|
{ prop: 'message', label: '信息', minWidth: 160 },
|
||||||
|
{ prop: 'userAgent', label: 'UA', minWidth: 200 },
|
||||||
|
{ prop: 'createdAt', label: '登录时间', width: 160, type: 'datetime' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
username: { type: 'string', title: '用户名', placeholder: '请输入用户名' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
title: '状态',
|
||||||
|
enum: ['success', 'fail'],
|
||||||
|
enumNames: ['成功', '失败'],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const searchQuery = reactive({ username: '', status: '' })
|
||||||
|
const appliedQuery = reactive({ username: '', status: '' })
|
||||||
|
|
||||||
|
const { data, loading, run } = useRequest(
|
||||||
|
(signal: AbortSignal) => listLoginLogs({ page: page.value, pageSize: pageSize.value, ...appliedQuery }, signal),
|
||||||
|
{ manual: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const list = computed(() => data.value?.list ?? [])
|
||||||
|
const total = computed(() => data.value?.total ?? 0)
|
||||||
|
|
||||||
|
function handleSearch(): void {
|
||||||
|
appliedQuery.username = searchQuery.username
|
||||||
|
appliedQuery.status = searchQuery.status
|
||||||
|
page.value = 1
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset(): void {
|
||||||
|
appliedQuery.username = ''
|
||||||
|
appliedQuery.status = ''
|
||||||
|
page.value = 1
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(run)
|
||||||
|
watch([page, pageSize], () => {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.log-page {
|
||||||
|
@apply flex h-full flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__search {
|
||||||
|
@apply mb-md flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__table {
|
||||||
|
@apply flex min-h-0 flex-1 flex-col;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
@apply flex min-h-0 flex-1 flex-col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-page__table-body {
|
||||||
|
@apply min-h-0 flex-1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -10,11 +10,13 @@
|
|||||||
:remove="remove"
|
:remove="remove"
|
||||||
id-key="id"
|
id-key="id"
|
||||||
name-key="key"
|
name-key="key"
|
||||||
|
:edit-readonly-keys="['key']"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
|
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||||
|
import { createConfig, deleteConfig, listConfigs, updateConfig, type ConfigListQuery } from '@/api/config'
|
||||||
|
|
||||||
const columns: TableColumn[] = [
|
const columns: TableColumn[] = [
|
||||||
{ prop: 'key', label: '键名', minWidth: 160 },
|
{ prop: 'key', label: '键名', minWidth: 160 },
|
||||||
@ -41,72 +43,18 @@ const formSchema: JsonSchema = {
|
|||||||
required: ['key'],
|
required: ['key'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 占位数据(后续对接后端时替换为 http 调用)----
|
function toListQuery(query: PageQuery): ConfigListQuery {
|
||||||
interface ConfigRow {
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||||||
id: string
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
remark: string
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
const day = 24 * 60 * 60 * 1000
|
|
||||||
const configs: ConfigRow[] = [
|
|
||||||
{ id: '1', key: 'sys.name', value: '投云系统管理平台', remark: '系统名称', createdAt: now - 30 * day },
|
|
||||||
{ id: '2', key: 'sys.theme', value: '#2E7FA3', remark: '默认主题色', createdAt: now - 20 * day },
|
|
||||||
{ id: '3', key: 'sys.copyright', value: '云南投云科技有限公司', remark: '版权信息', createdAt: now - 10 * day },
|
|
||||||
{ id: '4', key: 'sys.pageSize', value: '10', remark: '默认分页大小', createdAt: now - 5 * day },
|
|
||||||
{ id: '5', key: 'sys.captcha', value: 'true', remark: '是否开启验证码', createdAt: now - 1 * day },
|
|
||||||
]
|
|
||||||
|
|
||||||
let seq = 0
|
|
||||||
const nextId = () => `c-${Date.now()}-${++seq}`
|
|
||||||
|
|
||||||
function delay<T>(fn: () => T, ms = 300): Promise<T> {
|
|
||||||
return new Promise((resolve) => setTimeout(() => resolve(fn()), ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = (query: PageQuery): Promise<PageResult<Record<string, any>>> =>
|
|
||||||
delay(() => {
|
|
||||||
const { page, pageSize, key, value } = query
|
|
||||||
const filtered = configs.filter((c) => {
|
|
||||||
if (key && !c.key.includes(String(key))) return false
|
|
||||||
if (value && !c.value.includes(String(value))) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
const start = (page - 1) * pageSize
|
|
||||||
return {
|
return {
|
||||||
list: filtered.slice(start, start + pageSize) as unknown as Record<string, any>[],
|
page: query.page,
|
||||||
total: filtered.length,
|
pageSize: query.pageSize,
|
||||||
page,
|
key: str(query.key),
|
||||||
pageSize,
|
value: str(query.value),
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const create = (data: Record<string, any>): Promise<unknown> =>
|
const list = (query: PageQuery, signal?: AbortSignal) => listConfigs(toListQuery(query), signal)
|
||||||
delay(() => {
|
const create = (data: Record<string, any>) => createConfig(data)
|
||||||
configs.unshift({
|
const update = (id: string, data: Record<string, any>) => updateConfig(id, data)
|
||||||
id: nextId(),
|
const remove = (id: string) => deleteConfig(id)
|
||||||
key: data.key,
|
|
||||||
value: data.value ?? '',
|
|
||||||
remark: data.remark ?? '',
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = (id: string, data: Record<string, any>): Promise<unknown> =>
|
|
||||||
delay(() => {
|
|
||||||
const c = configs.find((x) => x.id === id)
|
|
||||||
if (c) Object.assign(c, data)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const remove = (id: string): Promise<unknown> =>
|
|
||||||
delay(() => {
|
|
||||||
const i = configs.findIndex((x) => x.id === id)
|
|
||||||
if (i >= 0) configs.splice(i, 1)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -57,124 +57,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
|
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||||
import TyForm from '@/components/ty-form.vue'
|
import TyForm from '@/components/ty-form.vue'
|
||||||
import { showDialog } from '@/utils/dialog'
|
import { showDialog } from '@/utils/dialog'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
|
import {
|
||||||
|
createDictItem,
|
||||||
|
createDictType,
|
||||||
|
deleteDictItem,
|
||||||
|
deleteDictType,
|
||||||
|
listDictItems,
|
||||||
|
listDictTypes,
|
||||||
|
updateDictItem,
|
||||||
|
updateDictType,
|
||||||
|
type DictType,
|
||||||
|
} from '@/api/dict'
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
enabled: { label: '启用', type: 'success' },
|
enabled: { label: '启用', type: 'success' },
|
||||||
disabled: { label: '停用', type: 'info' },
|
disabled: { label: '停用', type: 'info' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// ---- 占位数据(后续对接后端时替换为 http 调用)----
|
// ---- 字典类型(左) ----
|
||||||
interface DictTypeRow {
|
const { data: typePage, refresh: refreshTypes } = useRequest((signal: AbortSignal) => listDictTypes(1, 1000, signal))
|
||||||
id: string
|
const types = computed<DictType[]>(() => typePage.value?.list ?? [])
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
status: 'enabled' | 'disabled'
|
|
||||||
remark?: string
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DictItemRow {
|
|
||||||
id: string
|
|
||||||
dictTypeCode: string
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
sort: number
|
|
||||||
status: 'enabled' | 'disabled'
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
const day = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
const dictTypes: DictTypeRow[] = [
|
|
||||||
{ id: 'dt-1', code: 'sys_status', name: '系统状态', status: 'enabled', remark: '通用启用/停用状态', createdAt: now - 30 * day },
|
|
||||||
{ id: 'dt-2', code: 'sys_sex', name: '用户性别', status: 'enabled', remark: '性别选项', createdAt: now - 29 * day },
|
|
||||||
{ id: 'dt-3', code: 'menu_type', name: '菜单类型', status: 'enabled', remark: '菜单/按钮/接口', createdAt: now - 28 * day },
|
|
||||||
{ id: 'dt-4', code: 'oper_type', name: '操作类型', status: 'enabled', remark: '操作日志动作分类', createdAt: now - 27 * day },
|
|
||||||
{ id: 'dt-5', code: 'sys_yes_no', name: '系统是否', status: 'enabled', remark: '是否判断', createdAt: now - 26 * day },
|
|
||||||
{ id: 'dt-6', code: 'login_status', name: '登录状态', status: 'enabled', remark: '登录结果', createdAt: now - 25 * day },
|
|
||||||
{ id: 'dt-7', code: 'audit_status', name: '操作结果', status: 'enabled', remark: '审计结果', createdAt: now - 24 * day },
|
|
||||||
{ id: 'dt-8', code: 'notice_type', name: '通知类型', status: 'enabled', remark: '通知/公告', createdAt: now - 23 * day },
|
|
||||||
{ id: 'dt-9', code: 'job_group', name: '任务分组', status: 'enabled', remark: '定时任务分组', createdAt: now - 22 * day },
|
|
||||||
{ id: 'dt-10', code: 'job_status', name: '任务状态', status: 'enabled', remark: '任务运行状态', createdAt: now - 21 * day },
|
|
||||||
{ id: 'dt-11', code: 'tenant_status', name: '租户状态', status: 'enabled', remark: '租户启用/停用', createdAt: now - 20 * day },
|
|
||||||
{ id: 'dt-12', code: 'data_scope', name: '数据权限', status: 'enabled', remark: '角色数据权限档位', createdAt: now - 19 * day },
|
|
||||||
{ id: 'dt-13', code: 'log_type', name: '日志类型', status: 'enabled', remark: '操作/登录日志', createdAt: now - 18 * day },
|
|
||||||
{ id: 'dt-14', code: 'message_status', name: '消息状态', status: 'enabled', remark: '消息已读/未读', createdAt: now - 17 * day },
|
|
||||||
{ id: 'dt-15', code: 'priority_level', name: '优先级', status: 'enabled', remark: '任务优先级', createdAt: now - 16 * day },
|
|
||||||
{ id: 'dt-16', code: 'deploy_env', name: '部署环境', status: 'enabled', remark: '开发/测试/生产', createdAt: now - 15 * day },
|
|
||||||
{ id: 'dt-17', code: 'language', name: '语言', status: 'enabled', remark: '系统语言', createdAt: now - 14 * day },
|
|
||||||
{ id: 'dt-18', code: 'cache_type', name: '缓存类型', status: 'enabled', remark: '缓存介质', createdAt: now - 13 * day },
|
|
||||||
{ id: 'dt-19', code: 'report_type', name: '报表类型', status: 'disabled', remark: '预留:报表分类', createdAt: now - 12 * day },
|
|
||||||
{ id: 'dt-20', code: 'theme_mode', name: '主题模式', status: 'enabled', remark: '明亮/暗黑', createdAt: now - 11 * day },
|
|
||||||
]
|
|
||||||
|
|
||||||
const dictItems: DictItemRow[] = [
|
|
||||||
{ id: 'di-1', dictTypeCode: 'sys_status', label: '启用', value: 'enabled', sort: 1, status: 'enabled', createdAt: now - 10 * day },
|
|
||||||
{ id: 'di-2', dictTypeCode: 'sys_status', label: '停用', value: 'disabled', sort: 2, status: 'enabled', createdAt: now - 10 * day },
|
|
||||||
{ id: 'di-3', dictTypeCode: 'sys_sex', label: '男', value: 'male', sort: 1, status: 'enabled', createdAt: now - 9 * day },
|
|
||||||
{ id: 'di-4', dictTypeCode: 'sys_sex', label: '女', value: 'female', sort: 2, status: 'enabled', createdAt: now - 9 * day },
|
|
||||||
{ id: 'di-5', dictTypeCode: 'sys_sex', label: '未知', value: 'unknown', sort: 3, status: 'enabled', createdAt: now - 9 * day },
|
|
||||||
{ id: 'di-6', dictTypeCode: 'menu_type', label: '目录', value: 'dir', sort: 1, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-7', dictTypeCode: 'menu_type', label: '菜单', value: 'menu', sort: 2, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-8', dictTypeCode: 'menu_type', label: '按钮', value: 'button', sort: 3, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-9', dictTypeCode: 'menu_type', label: '接口', value: 'api', sort: 4, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-10', dictTypeCode: 'oper_type', label: '新增', value: 'create', sort: 1, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-11', dictTypeCode: 'oper_type', label: '修改', value: 'update', sort: 2, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-12', dictTypeCode: 'oper_type', label: '删除', value: 'delete', sort: 3, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-13', dictTypeCode: 'oper_type', label: '查询', value: 'query', sort: 4, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-14', dictTypeCode: 'sys_yes_no', label: '是', value: 'yes', sort: 1, status: 'enabled', createdAt: now - 9 * day },
|
|
||||||
{ id: 'di-15', dictTypeCode: 'sys_yes_no', label: '否', value: 'no', sort: 2, status: 'enabled', createdAt: now - 9 * day },
|
|
||||||
{ id: 'di-16', dictTypeCode: 'login_status', label: '成功', value: 'success', sort: 1, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-17', dictTypeCode: 'login_status', label: '失败', value: 'fail', sort: 2, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-18', dictTypeCode: 'audit_status', label: '成功', value: 'success', sort: 1, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-19', dictTypeCode: 'audit_status', label: '失败', value: 'fail', sort: 2, status: 'enabled', createdAt: now - 8 * day },
|
|
||||||
{ id: 'di-20', dictTypeCode: 'notice_type', label: '通知', value: 'notice', sort: 1, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-21', dictTypeCode: 'notice_type', label: '公告', value: 'announcement', sort: 2, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-22', dictTypeCode: 'job_group', label: '系统', value: 'system', sort: 1, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-23', dictTypeCode: 'job_group', label: '默认', value: 'default', sort: 2, status: 'enabled', createdAt: now - 7 * day },
|
|
||||||
{ id: 'di-24', dictTypeCode: 'job_status', label: '运行', value: 'running', sort: 1, status: 'enabled', createdAt: now - 6 * day },
|
|
||||||
{ id: 'di-25', dictTypeCode: 'job_status', label: '暂停', value: 'paused', sort: 2, status: 'enabled', createdAt: now - 6 * day },
|
|
||||||
{ id: 'di-26', dictTypeCode: 'tenant_status', label: '启用', value: 'enabled', sort: 1, status: 'enabled', createdAt: now - 6 * day },
|
|
||||||
{ id: 'di-27', dictTypeCode: 'tenant_status', label: '停用', value: 'disabled', sort: 2, status: 'enabled', createdAt: now - 6 * day },
|
|
||||||
{ id: 'di-28', dictTypeCode: 'data_scope', label: '全部', value: 'all', sort: 1, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-29', dictTypeCode: 'data_scope', label: '本部门', value: 'dept', sort: 2, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-30', dictTypeCode: 'data_scope', label: '本部门及以下', value: 'dept_and_child', sort: 3, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-31', dictTypeCode: 'data_scope', label: '本人', value: 'self', sort: 4, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-32', dictTypeCode: 'data_scope', label: '自定义', value: 'custom', sort: 5, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-33', dictTypeCode: 'log_type', label: '操作', value: 'operate', sort: 1, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-34', dictTypeCode: 'log_type', label: '登录', value: 'login', sort: 2, status: 'enabled', createdAt: now - 5 * day },
|
|
||||||
{ id: 'di-35', dictTypeCode: 'message_status', label: '未读', value: 'unread', sort: 1, status: 'enabled', createdAt: now - 4 * day },
|
|
||||||
{ id: 'di-36', dictTypeCode: 'message_status', label: '已读', value: 'read', sort: 2, status: 'enabled', createdAt: now - 4 * day },
|
|
||||||
{ id: 'di-37', dictTypeCode: 'priority_level', label: '低', value: 'low', sort: 1, status: 'enabled', createdAt: now - 4 * day },
|
|
||||||
{ id: 'di-38', dictTypeCode: 'priority_level', label: '中', value: 'medium', sort: 2, status: 'enabled', createdAt: now - 4 * day },
|
|
||||||
{ id: 'di-39', dictTypeCode: 'priority_level', label: '高', value: 'high', sort: 3, status: 'enabled', createdAt: now - 4 * day },
|
|
||||||
{ id: 'di-40', dictTypeCode: 'deploy_env', label: '开发', value: 'dev', sort: 1, status: 'enabled', createdAt: now - 3 * day },
|
|
||||||
{ id: 'di-41', dictTypeCode: 'deploy_env', label: '测试', value: 'test', sort: 2, status: 'enabled', createdAt: now - 3 * day },
|
|
||||||
{ id: 'di-42', dictTypeCode: 'deploy_env', label: '生产', value: 'prod', sort: 3, status: 'enabled', createdAt: now - 3 * day },
|
|
||||||
{ id: 'di-43', dictTypeCode: 'language', label: '简体中文', value: 'zh_CN', sort: 1, status: 'enabled', createdAt: now - 3 * day },
|
|
||||||
{ id: 'di-44', dictTypeCode: 'language', label: 'English', value: 'en_US', sort: 2, status: 'enabled', createdAt: now - 3 * day },
|
|
||||||
{ id: 'di-45', dictTypeCode: 'cache_type', label: 'Redis', value: 'redis', sort: 1, status: 'enabled', createdAt: now - 2 * day },
|
|
||||||
{ id: 'di-46', dictTypeCode: 'cache_type', label: '本地', value: 'local', sort: 2, status: 'enabled', createdAt: now - 2 * day },
|
|
||||||
{ id: 'di-47', dictTypeCode: 'theme_mode', label: '明亮', value: 'light', sort: 1, status: 'enabled', createdAt: now - 1 * day },
|
|
||||||
{ id: 'di-48', dictTypeCode: 'theme_mode', label: '暗黑', value: 'dark', sort: 2, status: 'enabled', createdAt: now - 1 * day },
|
|
||||||
]
|
|
||||||
|
|
||||||
const typeKeyword = ref('')
|
const typeKeyword = ref('')
|
||||||
const selectedType = ref<DictTypeRow | null>(dictTypes[0] ?? null)
|
const selectedType = ref<DictType | null>(null)
|
||||||
|
|
||||||
const filteredTypes = computed(() => {
|
const filteredTypes = computed(() => {
|
||||||
const kw = typeKeyword.value.trim().toLowerCase()
|
const kw = typeKeyword.value.trim().toLowerCase()
|
||||||
if (!kw) return dictTypes
|
if (!kw) return types.value
|
||||||
return dictTypes.filter((t) => t.name.toLowerCase().includes(kw) || t.code.toLowerCase().includes(kw))
|
return types.value.filter((t) => t.name.toLowerCase().includes(kw) || t.code.toLowerCase().includes(kw))
|
||||||
})
|
})
|
||||||
|
|
||||||
function selectType(t: DictTypeRow): void {
|
// 类型加载后默认选中第一个;当前选中被删后回退到第一个
|
||||||
|
watch(types, (list) => {
|
||||||
|
if (!selectedType.value) selectedType.value = list[0] ?? null
|
||||||
|
else if (!list.some((t) => t.id === selectedType.value!.id)) selectedType.value = list[0] ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectType(t: DictType): void {
|
||||||
selectedType.value = t
|
selectedType.value = t
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,19 +115,17 @@ const typeFormSchema: JsonSchema = {
|
|||||||
enum: ['enabled', 'disabled'],
|
enum: ['enabled', 'disabled'],
|
||||||
enumNames: ['启用', '停用'],
|
enumNames: ['启用', '停用'],
|
||||||
default: 'enabled',
|
default: 'enabled',
|
||||||
|
showOn: ['edit'],
|
||||||
},
|
},
|
||||||
remark: { type: 'string', title: '备注', format: 'textarea', placeholder: '请输入备注' },
|
remark: { type: 'string', title: '备注', format: 'textarea', placeholder: '请输入备注' },
|
||||||
},
|
},
|
||||||
required: ['code', 'name'],
|
required: ['code', 'name'],
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTypeModel(t?: DictTypeRow): Record<string, any> {
|
function buildTypeModel(t?: DictType): Record<string, any> {
|
||||||
return { code: t?.code ?? '', name: t?.name ?? '', status: t?.status ?? 'enabled', remark: t?.remark ?? '' }
|
return { code: t?.code ?? '', name: t?.name ?? '', status: t?.status ?? 'enabled', remark: t?.remark ?? '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
let typeSeq = 0
|
|
||||||
const nextTypeId = () => `dt-${Date.now()}-${++typeSeq}`
|
|
||||||
|
|
||||||
async function openTypeCreate(): Promise<void> {
|
async function openTypeCreate(): Promise<void> {
|
||||||
const data = await showDialog({
|
const data = await showDialog({
|
||||||
component: TyForm,
|
component: TyForm,
|
||||||
@ -211,19 +134,13 @@ async function openTypeCreate(): Promise<void> {
|
|||||||
width: '480px',
|
width: '480px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
dictTypes.push({
|
await createDictType({ code: data.code, name: data.name, remark: data.remark })
|
||||||
id: nextTypeId(),
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
status: data.status,
|
|
||||||
remark: data.remark,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
selectedType.value = dictTypes[dictTypes.length - 1]
|
await refreshTypes()
|
||||||
|
selectedType.value = types.value.find((t) => t.code === data.code) ?? selectedType.value
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openTypeEdit(t: DictTypeRow): Promise<void> {
|
async function openTypeEdit(t: DictType): Promise<void> {
|
||||||
const data = await showDialog({
|
const data = await showDialog({
|
||||||
component: TyForm,
|
component: TyForm,
|
||||||
props: { schema: typeFormSchema, modelValue: buildTypeModel(t), mode: 'edit', readonlyKeys: ['code'] },
|
props: { schema: typeFormSchema, modelValue: buildTypeModel(t), mode: 'edit', readonlyKeys: ['code'] },
|
||||||
@ -231,22 +148,19 @@ async function openTypeEdit(t: DictTypeRow): Promise<void> {
|
|||||||
width: '480px',
|
width: '480px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
Object.assign(t, data)
|
await updateDictType(t.id, { name: data.name, status: data.status, remark: data.remark })
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
|
await refreshTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeType(t: DictTypeRow): Promise<void> {
|
async function removeType(t: DictType): Promise<void> {
|
||||||
await ElMessageBox.confirm(`确认删除字典类型「${t.name}」及其全部字典项?`, '提示', { type: 'warning' })
|
await ElMessageBox.confirm(`确认删除字典类型「${t.name}」?`, '提示', { type: 'warning' })
|
||||||
const ti = dictTypes.findIndex((x) => x.id === t.id)
|
await deleteDictType(t.id)
|
||||||
if (ti >= 0) dictTypes.splice(ti, 1)
|
|
||||||
for (let i = dictItems.length - 1; i >= 0; i--) {
|
|
||||||
if (dictItems[i].dictTypeCode === t.code) dictItems.splice(i, 1)
|
|
||||||
}
|
|
||||||
if (selectedType.value?.id === t.id) selectedType.value = dictTypes[0] ?? null
|
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
await refreshTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 字典项 ----
|
// ---- 字典项(右) ----
|
||||||
const itemColumns: TableColumn[] = [
|
const itemColumns: TableColumn[] = [
|
||||||
{ prop: 'label', label: '标签', minWidth: 120 },
|
{ prop: 'label', label: '标签', minWidth: 120 },
|
||||||
{ prop: 'value', label: '值', minWidth: 120 },
|
{ prop: 'value', label: '值', minWidth: 120 },
|
||||||
@ -275,64 +189,42 @@ const itemFormSchema: JsonSchema = {
|
|||||||
enum: ['enabled', 'disabled'],
|
enum: ['enabled', 'disabled'],
|
||||||
enumNames: ['启用', '停用'],
|
enumNames: ['启用', '停用'],
|
||||||
default: 'enabled',
|
default: 'enabled',
|
||||||
|
showOn: ['edit'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['label', 'value'],
|
required: ['label', 'value'],
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemSeq = 0
|
const listItems = async (query: PageQuery, signal?: AbortSignal) => {
|
||||||
const nextItemId = () => `di-${Date.now()}-${++itemSeq}`
|
const code = selectedType.value?.code
|
||||||
|
if (!code) return { list: [], total: 0, page: query.page, pageSize: query.pageSize }
|
||||||
function delay<T>(fn: () => T, ms = 300): Promise<T> {
|
const items = await listDictItems(code, signal)
|
||||||
return new Promise((resolve) => setTimeout(() => resolve(fn()), ms))
|
const filtered = items.filter((i) => {
|
||||||
}
|
if (query.label && !i.label.includes(String(query.label))) return false
|
||||||
|
if (query.value && !i.value.includes(String(query.value))) return false
|
||||||
const listItems = (query: PageQuery): Promise<PageResult<Record<string, any>>> =>
|
|
||||||
delay(() => {
|
|
||||||
const code = selectedType.value?.code ?? ''
|
|
||||||
const { page, pageSize, label, value } = query
|
|
||||||
const filtered = dictItems.filter((i) => {
|
|
||||||
if (i.dictTypeCode !== code) return false
|
|
||||||
if (label && !i.label.includes(String(label))) return false
|
|
||||||
if (value && !i.value.includes(String(value))) return false
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
const start = (page - 1) * pageSize
|
const start = (query.page - 1) * query.pageSize
|
||||||
return {
|
return {
|
||||||
list: filtered.slice(start, start + pageSize) as unknown as Record<string, any>[],
|
list: filtered.slice(start, start + query.pageSize) as unknown as Record<string, any>[],
|
||||||
total: filtered.length,
|
total: filtered.length,
|
||||||
page,
|
page: query.page,
|
||||||
pageSize,
|
pageSize: query.pageSize,
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const createItem = (data: Record<string, any>): Promise<unknown> =>
|
const createItem = (data: Record<string, any>) =>
|
||||||
delay(() => {
|
createDictItem({
|
||||||
dictItems.push({
|
|
||||||
id: nextItemId(),
|
|
||||||
dictTypeCode: selectedType.value?.code ?? '',
|
dictTypeCode: selectedType.value?.code ?? '',
|
||||||
label: data.label,
|
label: data.label,
|
||||||
value: data.value,
|
value: data.value,
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
status: data.status,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateItem = (id: string, data: Record<string, any>): Promise<unknown> =>
|
const updateItem = (id: string, data: Record<string, any>) =>
|
||||||
delay(() => {
|
updateDictItem(id, { label: data.label, value: data.value, sort: data.sort ?? 0, status: data.status })
|
||||||
const it = dictItems.find((x) => x.id === id)
|
|
||||||
if (it) Object.assign(it, data)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeItem = (id: string): Promise<unknown> =>
|
const removeItem = (id: string) => deleteDictItem(id)
|
||||||
delay(() => {
|
|
||||||
const i = dictItems.findIndex((x) => x.id === id)
|
|
||||||
if (i >= 0) dictItems.splice(i, 1)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -44,48 +44,19 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import type { JsonSchema, TableColumn } from '@/types/crud'
|
import type { JsonSchema, TableColumn } from '@/types/crud'
|
||||||
import TyForm from '@/components/ty-form.vue'
|
import TyForm from '@/components/ty-form.vue'
|
||||||
import { showDialog } from '@/utils/dialog'
|
import { showDialog } from '@/utils/dialog'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
||||||
|
import { createOrg, deleteOrg, listOrgs, updateOrg, type Org } from '@/api/org'
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
enabled: { label: '启用', type: 'success' },
|
enabled: { label: '启用', type: 'success' },
|
||||||
disabled: { label: '停用', type: 'info' },
|
disabled: { label: '停用', type: 'info' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// ---- 占位数据(后续对接后端时替换为 http 调用)----
|
type OrgNode = TreeNode<Org>
|
||||||
interface OrgRow {
|
|
||||||
id: string
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
parentCode: string | null
|
|
||||||
leader?: string
|
|
||||||
sort: number
|
|
||||||
status: 'enabled' | 'disabled'
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrgNode = TreeNode<OrgRow>
|
const { data: orgList, refresh: refreshOrgs } = useRequest((signal: AbortSignal) => listOrgs(signal))
|
||||||
|
const orgs = computed<Org[]>(() => orgList.value ?? [])
|
||||||
const now = Date.now()
|
|
||||||
const day = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
const orgs = ref<OrgRow[]>([
|
|
||||||
{ id: 'o-1', code: 'touyun', name: '投云总部', parentCode: null, leader: '王总', sort: 1, status: 'enabled', createdAt: now - 365 * day },
|
|
||||||
{ id: 'o-2', code: 'rd', name: '研发部', parentCode: 'touyun', leader: '张经理', sort: 1, status: 'enabled', createdAt: now - 300 * day },
|
|
||||||
{ id: 'o-3', code: 'fe', name: '前端组', parentCode: 'rd', leader: '李组长', sort: 1, status: 'enabled', createdAt: now - 200 * day },
|
|
||||||
{ id: 'o-4', code: 'be', name: '后端组', parentCode: 'rd', leader: '赵组长', sort: 2, status: 'enabled', createdAt: now - 190 * day },
|
|
||||||
{ id: 'o-5', code: 'qa', name: '测试组', parentCode: 'rd', leader: '孙组长', sort: 3, status: 'enabled', createdAt: now - 180 * day },
|
|
||||||
{ id: 'o-6', code: 'ops', name: '运维部', parentCode: 'touyun', leader: '周经理', sort: 2, status: 'enabled', createdAt: now - 280 * day },
|
|
||||||
{ id: 'o-7', code: 'dba', name: '数据库组', parentCode: 'ops', leader: '吴组长', sort: 1, status: 'enabled', createdAt: now - 150 * day },
|
|
||||||
{ id: 'o-8', code: 'net', name: '网络组', parentCode: 'ops', leader: '郑组长', sort: 2, status: 'enabled', createdAt: now - 140 * day },
|
|
||||||
{ id: 'o-9', code: 'sales', name: '销售部', parentCode: 'touyun', leader: '钱经理', sort: 3, status: 'enabled', createdAt: now - 260 * day },
|
|
||||||
{ id: 'o-10', code: 'east', name: '华东大区', parentCode: 'sales', leader: '陈总监', sort: 1, status: 'enabled', createdAt: now - 120 * day },
|
|
||||||
{ id: 'o-11', code: 'south', name: '华南大区', parentCode: 'sales', leader: '林总监', sort: 2, status: 'enabled', createdAt: now - 110 * day },
|
|
||||||
{ id: 'o-12', code: 'hr', name: '人事部', parentCode: 'touyun', leader: '何经理', sort: 4, status: 'enabled', createdAt: now - 200 * day },
|
|
||||||
{ id: 'o-13', code: 'fin', name: '财务部', parentCode: 'touyun', leader: '许经理', sort: 5, status: 'enabled', createdAt: now - 180 * day },
|
|
||||||
{ id: 'o-14', code: 'pm', name: '产品部', parentCode: 'touyun', leader: '邓经理', sort: 6, status: 'disabled', createdAt: now - 90 * day },
|
|
||||||
{ id: 'o-15', code: 'marketing', name: '市场部', parentCode: 'touyun', leader: '冯经理', sort: 7, status: 'enabled', createdAt: now - 100 * day },
|
|
||||||
{ id: 'o-16', code: 'brand', name: '品牌组', parentCode: 'marketing', leader: '蒋组长', sort: 1, status: 'enabled', createdAt: now - 60 * day },
|
|
||||||
])
|
|
||||||
|
|
||||||
const columns: TableColumn[] = [
|
const columns: TableColumn[] = [
|
||||||
{ prop: 'name', label: '组织名称', minWidth: 220 },
|
{ prop: 'name', label: '组织名称', minWidth: 220 },
|
||||||
@ -168,13 +139,14 @@ function orgFormSchema(excludeCode?: string): JsonSchema {
|
|||||||
enum: ['enabled', 'disabled'],
|
enum: ['enabled', 'disabled'],
|
||||||
enumNames: ['启用', '停用'],
|
enumNames: ['启用', '停用'],
|
||||||
default: 'enabled',
|
default: 'enabled',
|
||||||
|
showOn: ['edit'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['code', 'name'],
|
required: ['code', 'name'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOrgModel(o?: OrgRow): Record<string, any> {
|
function buildOrgModel(o?: Org): Record<string, any> {
|
||||||
return {
|
return {
|
||||||
parentCode: o?.parentCode ?? '',
|
parentCode: o?.parentCode ?? '',
|
||||||
code: o?.code ?? '',
|
code: o?.code ?? '',
|
||||||
@ -185,13 +157,6 @@ function buildOrgModel(o?: OrgRow): Record<string, any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let seq = 0
|
|
||||||
const nextId = () => `o-${Date.now()}-${++seq}`
|
|
||||||
|
|
||||||
function delay<T>(fn: () => T, ms = 300): Promise<T> {
|
|
||||||
return new Promise((resolve) => setTimeout(() => resolve(fn()), ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openCreate(): Promise<void> {
|
async function openCreate(): Promise<void> {
|
||||||
const data = await showDialog({
|
const data = await showDialog({
|
||||||
component: TyForm,
|
component: TyForm,
|
||||||
@ -200,20 +165,15 @@ async function openCreate(): Promise<void> {
|
|||||||
width: '480px',
|
width: '480px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
await createOrg({
|
||||||
orgs.value.push({
|
|
||||||
id: nextId(),
|
|
||||||
code: data.code,
|
code: data.code,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
parentCode: data.parentCode || null,
|
parentCode: data.parentCode || null,
|
||||||
leader: data.leader,
|
leader: data.leader,
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
status: data.status,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
||||||
@ -226,24 +186,19 @@ async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
|||||||
width: '480px',
|
width: '480px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
await createOrg({
|
||||||
orgs.value.push({
|
|
||||||
id: nextId(),
|
|
||||||
code: data.code,
|
code: data.code,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
parentCode: data.parentCode || parent.code,
|
parentCode: data.parentCode || parent.code,
|
||||||
leader: data.leader,
|
leader: data.leader,
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
status: data.status,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
})
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEdit(row: Record<string, any>): Promise<void> {
|
async function openEdit(row: Record<string, any>): Promise<void> {
|
||||||
const o = row as OrgRow
|
const o = row as Org
|
||||||
const data = await showDialog({
|
const data = await showDialog({
|
||||||
component: TyForm,
|
component: TyForm,
|
||||||
props: {
|
props: {
|
||||||
@ -256,28 +211,25 @@ async function openEdit(row: Record<string, any>): Promise<void> {
|
|||||||
width: '480px',
|
width: '480px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
await updateOrg(o.id, {
|
||||||
const target = orgs.value.find((x) => x.id === o.id)
|
|
||||||
if (target) {
|
|
||||||
Object.assign(target, {
|
|
||||||
parentCode: data.parentCode || null,
|
parentCode: data.parentCode || null,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
leader: data.leader,
|
leader: data.leader,
|
||||||
sort: data.sort ?? 0,
|
sort: data.sort ?? 0,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(row: Record<string, any>): Promise<void> {
|
async function handleDelete(row: Record<string, any>): Promise<void> {
|
||||||
const o = row as OrgRow
|
const o = row as Org
|
||||||
await ElMessageBox.confirm(`确认删除组织「${o.name}」及其所有下级组织?`, '提示', { type: 'warning' })
|
await ElMessageBox.confirm(`确认删除组织「${o.name}」及其所有下级组织?`, '提示', { type: 'warning' })
|
||||||
const codes = new Set([o.code, ...collectDescendants(orgs.value, o.code)])
|
const codes = new Set([o.code, ...collectDescendants(orgs.value, o.code)])
|
||||||
orgs.value = orgs.value.filter((x) => !codes.has(x.code))
|
const targets = orgs.value.filter((x) => codes.has(x.code))
|
||||||
|
for (const t of targets) await deleteOrg(t.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRows = ref<Record<string, any>[]>([])
|
const selectedRows = ref<Record<string, any>[]>([])
|
||||||
@ -293,9 +245,11 @@ async function batchDelete(rows: Record<string, any>[]): Promise<void> {
|
|||||||
codes.add(String(r.code))
|
codes.add(String(r.code))
|
||||||
for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c)
|
for (const c of collectDescendants(orgs.value, String(r.code))) codes.add(c)
|
||||||
}
|
}
|
||||||
orgs.value = orgs.value.filter((o) => !codes.has(o.code))
|
const targets = orgs.value.filter((o) => codes.has(o.code))
|
||||||
|
for (const t of targets) await deleteOrg(t.id)
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
await refreshOrgs()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,9 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import type { JsonSchema, TableColumn } from '@/types/crud'
|
import type { JsonSchema, TableColumn } from '@/types/crud'
|
||||||
import TyForm from '@/components/ty-form.vue'
|
import TyForm from '@/components/ty-form.vue'
|
||||||
import { showDialog } from '@/utils/dialog'
|
import { showDialog } from '@/utils/dialog'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
||||||
|
import { createResource, deleteResource, listResources, updateResource, type Resource, type ResourceUpsertData } from '@/api/resource'
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
enabled: { label: '启用', type: 'success' },
|
enabled: { label: '启用', type: 'success' },
|
||||||
@ -57,45 +59,10 @@ const TYPE_MAP = {
|
|||||||
api: { label: '接口', type: 'info' },
|
api: { label: '接口', type: 'info' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// ---- 占位数据(后续对接后端时替换为 http 调用)----
|
type ResourceNode = TreeNode<Resource>
|
||||||
interface ResourceRow {
|
|
||||||
id: string
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
parentCode: string | null
|
|
||||||
type: 'menu' | 'button' | 'api'
|
|
||||||
path?: string
|
|
||||||
component?: string
|
|
||||||
method?: string
|
|
||||||
apiPath?: string
|
|
||||||
icon?: string
|
|
||||||
sort: number
|
|
||||||
status: 'enabled' | 'disabled'
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceNode = TreeNode<ResourceRow>
|
const { data: resourceList, refresh: refreshResources } = useRequest((signal: AbortSignal) => listResources(signal))
|
||||||
|
const resources = computed<Resource[]>(() => resourceList.value ?? [])
|
||||||
const now = Date.now()
|
|
||||||
const day = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
const resources = ref<ResourceRow[]>([
|
|
||||||
{ id: 'r-1', code: 'dashboard', name: '工作台', parentCode: null, type: 'menu', path: '/', component: 'dashboard/index', icon: 'mdi:view-dashboard', sort: 1, status: 'enabled', createdAt: now - 60 * day },
|
|
||||||
{ id: 'r-2', code: 'system', name: '系统管理', parentCode: null, type: 'menu', path: '/system', component: 'Layout', icon: 'mdi:cog', sort: 2, status: 'enabled', createdAt: now - 58 * day },
|
|
||||||
{ id: 'r-3', code: 'system:user', name: '用户管理', parentCode: 'system', type: 'menu', path: '/system/user', component: 'system/user/index', icon: 'mdi:account-group', sort: 1, status: 'enabled', createdAt: now - 55 * day },
|
|
||||||
{ id: 'r-4', code: 'system:user:create', name: '新增用户', parentCode: 'system:user', type: 'button', sort: 1, status: 'enabled', createdAt: now - 55 * day },
|
|
||||||
{ id: 'r-5', code: 'system:user:update', name: '编辑用户', parentCode: 'system:user', type: 'button', sort: 2, status: 'enabled', createdAt: now - 55 * day },
|
|
||||||
{ id: 'r-6', code: 'system:user:delete', name: '删除用户', parentCode: 'system:user', type: 'button', sort: 3, status: 'enabled', createdAt: now - 55 * day },
|
|
||||||
{ id: 'r-7', code: 'system:user:list', name: '用户列表接口', parentCode: 'system:user', type: 'api', method: 'GET', apiPath: '/users', sort: 4, status: 'enabled', createdAt: now - 55 * day },
|
|
||||||
{ id: 'r-8', code: 'system:role', name: '角色管理', parentCode: 'system', type: 'menu', path: '/system/role', component: 'system/role/index', icon: 'mdi:account-key', sort: 2, status: 'enabled', createdAt: now - 50 * day },
|
|
||||||
{ id: 'r-9', code: 'system:resource', name: '资源管理', parentCode: 'system', type: 'menu', path: '/system/resource', component: 'system/resource/index', icon: 'mdi:view-list', sort: 3, status: 'enabled', createdAt: now - 48 * day },
|
|
||||||
{ id: 'r-10', code: 'system:org', name: '组织管理', parentCode: 'system', type: 'menu', path: '/system/org', component: 'system/org/index', icon: 'mdi:sitemap', sort: 4, status: 'enabled', createdAt: now - 46 * day },
|
|
||||||
{ id: 'r-11', code: 'system:dict', name: '字典管理', parentCode: 'system', type: 'menu', path: '/system/dict', component: 'system/dict/index', icon: 'mdi:book-open-variant', sort: 5, status: 'enabled', createdAt: now - 44 * day },
|
|
||||||
{ id: 'r-12', code: 'system:config', name: '参数配置', parentCode: 'system', type: 'menu', path: '/system/config', component: 'system/config/index', icon: 'mdi:tune', sort: 6, status: 'enabled', createdAt: now - 42 * day },
|
|
||||||
{ id: 'r-13', code: 'monitor', name: '系统监控', parentCode: null, type: 'menu', path: '/monitor', component: 'Layout', icon: 'mdi:monitor-dashboard', sort: 3, status: 'enabled', createdAt: now - 56 * day },
|
|
||||||
{ id: 'r-14', code: 'monitor:audit', name: '操作日志', parentCode: 'monitor', type: 'menu', path: '/monitor/audit', component: 'monitor/audit/index', icon: 'mdi:clipboard-text', sort: 1, status: 'enabled', createdAt: now - 40 * day },
|
|
||||||
{ id: 'r-15', code: 'monitor:login-log', name: '登录日志', parentCode: 'monitor', type: 'menu', path: '/monitor/login-log', component: 'monitor/login-log/index', icon: 'mdi:login', sort: 2, status: 'enabled', createdAt: now - 38 * day },
|
|
||||||
])
|
|
||||||
|
|
||||||
function pathDisplay(_value: unknown, row: Record<string, unknown>): string {
|
function pathDisplay(_value: unknown, row: Record<string, unknown>): string {
|
||||||
const type = row.type
|
const type = row.type
|
||||||
@ -221,14 +188,15 @@ function resourceFormSchema(excludeCode?: string): JsonSchema {
|
|||||||
enum: ['enabled', 'disabled'],
|
enum: ['enabled', 'disabled'],
|
||||||
enumNames: ['启用', '停用'],
|
enumNames: ['启用', '停用'],
|
||||||
default: 'enabled',
|
default: 'enabled',
|
||||||
|
showOn: ['edit'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['code', 'name', 'type'],
|
required: ['code', 'name', 'type'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildResourceModel(r?: ResourceRow): Record<string, any> {
|
function buildResourceModel(r?: Resource): Record<string, any> {
|
||||||
return reactive({
|
return {
|
||||||
parentCode: r?.parentCode ?? '',
|
parentCode: r?.parentCode ?? '',
|
||||||
type: r?.type ?? 'menu',
|
type: r?.type ?? 'menu',
|
||||||
code: r?.code ?? '',
|
code: r?.code ?? '',
|
||||||
@ -240,14 +208,27 @@ function buildResourceModel(r?: ResourceRow): Record<string, any> {
|
|||||||
apiPath: r?.apiPath ?? '',
|
apiPath: r?.apiPath ?? '',
|
||||||
sort: r?.sort ?? 0,
|
sort: r?.sort ?? 0,
|
||||||
status: r?.status ?? 'enabled',
|
status: r?.status ?? 'enabled',
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let seq = 0
|
/** 按类型归一化表单数据:菜单只带 path/component/icon,接口只带 method/apiPath。 */
|
||||||
const nextId = () => `r-${Date.now()}-${++seq}`
|
function toUpsertData(data: Record<string, any>): ResourceUpsertData {
|
||||||
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||||||
function delay<T>(fn: () => T, ms = 300): Promise<T> {
|
const isMenu = data.type === 'menu'
|
||||||
return new Promise((resolve) => setTimeout(() => resolve(fn()), ms))
|
const isApi = data.type === 'api'
|
||||||
|
return {
|
||||||
|
type: data.type,
|
||||||
|
code: str(data.code),
|
||||||
|
name: str(data.name),
|
||||||
|
parentCode: str(data.parentCode) ?? null,
|
||||||
|
path: isMenu ? str(data.path) : undefined,
|
||||||
|
component: isMenu ? str(data.component) : undefined,
|
||||||
|
icon: isMenu ? str(data.icon) : undefined,
|
||||||
|
method: isApi ? str(data.method) : undefined,
|
||||||
|
apiPath: isApi ? str(data.apiPath) : undefined,
|
||||||
|
sort: data.sort ?? 0,
|
||||||
|
status: str(data.status),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCreate(): Promise<void> {
|
async function openCreate(): Promise<void> {
|
||||||
@ -258,25 +239,9 @@ async function openCreate(): Promise<void> {
|
|||||||
width: '520px',
|
width: '520px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
await createResource(toUpsertData(data))
|
||||||
resources.value.push({
|
|
||||||
id: nextId(),
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
parentCode: data.parentCode || null,
|
|
||||||
type: data.type,
|
|
||||||
path: data.type === 'menu' ? data.path : undefined,
|
|
||||||
component: data.type === 'menu' ? data.component : undefined,
|
|
||||||
icon: data.type === 'menu' ? data.icon : undefined,
|
|
||||||
method: data.type === 'api' ? data.method : undefined,
|
|
||||||
apiPath: data.type === 'api' ? data.apiPath : undefined,
|
|
||||||
sort: data.sort ?? 0,
|
|
||||||
status: data.status,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
|
await refreshResources()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
||||||
@ -289,68 +254,40 @@ async function openCreateChild(parent: Record<string, any>): Promise<void> {
|
|||||||
width: '520px',
|
width: '520px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
const upsert = toUpsertData(data)
|
||||||
resources.value.push({
|
upsert.parentCode = upsert.parentCode || parent.code
|
||||||
id: nextId(),
|
await createResource(upsert)
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
parentCode: data.parentCode || parent.code,
|
|
||||||
type: data.type,
|
|
||||||
path: data.type === 'menu' ? data.path : undefined,
|
|
||||||
component: data.type === 'menu' ? data.component : undefined,
|
|
||||||
icon: data.type === 'menu' ? data.icon : undefined,
|
|
||||||
method: data.type === 'api' ? data.method : undefined,
|
|
||||||
apiPath: data.type === 'api' ? data.apiPath : undefined,
|
|
||||||
sort: data.sort ?? 0,
|
|
||||||
status: data.status,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
|
await refreshResources()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEdit(row: Record<string, any>): Promise<void> {
|
async function openEdit(row: Record<string, any>): Promise<void> {
|
||||||
const o = row as ResourceRow
|
const o = row as Resource
|
||||||
const data = await showDialog({
|
const data = await showDialog({
|
||||||
component: TyForm,
|
component: TyForm,
|
||||||
props: {
|
props: {
|
||||||
schema: resourceFormSchema(o.code),
|
schema: resourceFormSchema(o.code),
|
||||||
modelValue: buildResourceModel(o),
|
modelValue: buildResourceModel(o),
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
readonlyKeys: ['code'],
|
readonlyKeys: ['code', 'type'],
|
||||||
},
|
},
|
||||||
title: '编辑资源',
|
title: '编辑资源',
|
||||||
width: '520px',
|
width: '520px',
|
||||||
})
|
})
|
||||||
if (data == null) return
|
if (data == null) return
|
||||||
await delay(() => {
|
await updateResource(o.id, toUpsertData(data))
|
||||||
const r = resources.value.find((x) => x.id === o.id)
|
|
||||||
if (r) {
|
|
||||||
Object.assign(r, {
|
|
||||||
parentCode: data.parentCode || null,
|
|
||||||
type: data.type,
|
|
||||||
name: data.name,
|
|
||||||
path: data.type === 'menu' ? data.path : undefined,
|
|
||||||
component: data.type === 'menu' ? data.component : undefined,
|
|
||||||
icon: data.type === 'menu' ? data.icon : undefined,
|
|
||||||
method: data.type === 'api' ? data.method : undefined,
|
|
||||||
apiPath: data.type === 'api' ? data.apiPath : undefined,
|
|
||||||
sort: data.sort ?? 0,
|
|
||||||
status: data.status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功')
|
||||||
|
await refreshResources()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(row: Record<string, any>): Promise<void> {
|
async function handleDelete(row: Record<string, any>): Promise<void> {
|
||||||
const o = row as ResourceRow
|
const o = row as Resource
|
||||||
await ElMessageBox.confirm(`确认删除资源「${o.name}」及其所有下级资源?`, '提示', { type: 'warning' })
|
await ElMessageBox.confirm(`确认删除资源「${o.name}」及其所有下级资源?`, '提示', { type: 'warning' })
|
||||||
const codes = new Set([o.code, ...collectDescendants(resources.value, o.code)])
|
const codes = new Set([o.code, ...collectDescendants(resources.value, o.code)])
|
||||||
resources.value = resources.value.filter((x) => !codes.has(x.code))
|
const targets = resources.value.filter((x) => codes.has(x.code))
|
||||||
|
for (const t of targets) await deleteResource(t.id)
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
await refreshResources()
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRows = ref<Record<string, any>[]>([])
|
const selectedRows = ref<Record<string, any>[]>([])
|
||||||
@ -366,9 +303,11 @@ async function batchDelete(rows: Record<string, any>[]): Promise<void> {
|
|||||||
codes.add(String(r.code))
|
codes.add(String(r.code))
|
||||||
for (const c of collectDescendants(resources.value, String(r.code))) codes.add(c)
|
for (const c of collectDescendants(resources.value, String(r.code))) codes.add(c)
|
||||||
}
|
}
|
||||||
resources.value = resources.value.filter((o) => !codes.has(o.code))
|
const targets = resources.value.filter((o) => codes.has(o.code))
|
||||||
|
for (const t of targets) await deleteResource(t.id)
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
|
await refreshResources()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
53
src/pages/system/role/assign-resources-form.vue
Normal file
53
src/pages/system/role/assign-resources-form.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree
|
||||||
|
v-if="ready"
|
||||||
|
ref="treeRef"
|
||||||
|
class="assign-tree"
|
||||||
|
:data="tree"
|
||||||
|
node-key="code"
|
||||||
|
show-checkbox
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
:default-checked-keys="assigned ?? []"
|
||||||
|
default-expand-all
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRequest } from '@/composables/useRequest'
|
||||||
|
import { listResources } from '@/api/resource'
|
||||||
|
import { listAssignedResources } from '@/api/role'
|
||||||
|
import { buildTree, type TreeNode } from '@/utils/tree'
|
||||||
|
import type { Resource } from '@/api/resource'
|
||||||
|
|
||||||
|
defineOptions({ name: 'AssignResourcesForm' })
|
||||||
|
|
||||||
|
const props = defineProps<{ roleCode: string }>()
|
||||||
|
|
||||||
|
interface TreeApi {
|
||||||
|
getCheckedKeys: () => (string | number)[]
|
||||||
|
getHalfCheckedKeys: () => (string | number)[]
|
||||||
|
}
|
||||||
|
const treeRef = ref<TreeApi>()
|
||||||
|
|
||||||
|
const { data: resourceList } = useRequest((signal: AbortSignal) => listResources(signal))
|
||||||
|
const { data: assigned } = useRequest((signal: AbortSignal) => listAssignedResources(props.roleCode, signal))
|
||||||
|
|
||||||
|
const tree = computed(() => buildTree(resourceList.value ?? []) as TreeNode<Resource>[])
|
||||||
|
const ready = computed(() => resourceList.value !== undefined && assigned.value !== undefined)
|
||||||
|
|
||||||
|
function submit(): { resourceCodes: string[] } {
|
||||||
|
const checked = treeRef.value?.getCheckedKeys() ?? []
|
||||||
|
const half = treeRef.value?.getHalfCheckedKeys() ?? []
|
||||||
|
return { resourceCodes: [...new Set([...checked, ...half].map(String))] }
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ submit })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.assign-tree {
|
||||||
|
max-height: 420px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
122
src/pages/system/role/index.vue
Normal file
122
src/pages/system/role/index.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<ty-crud
|
||||||
|
entity-name="角色"
|
||||||
|
:columns="columns"
|
||||||
|
:search-schema="searchSchema"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:list="list"
|
||||||
|
:create="create"
|
||||||
|
:update="update"
|
||||||
|
:remove="remove"
|
||||||
|
:actions="actions"
|
||||||
|
id-key="id"
|
||||||
|
name-key="name"
|
||||||
|
:edit-readonly-keys="['code']"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||||
|
import { showDialog } from '@/utils/dialog'
|
||||||
|
import { assignResources, createRole, deleteRole, listRoles, updateRole, type RoleListQuery } from '@/api/role'
|
||||||
|
import AssignResourcesForm from './assign-resources-form.vue'
|
||||||
|
|
||||||
|
const STATUS_MAP = {
|
||||||
|
enabled: { label: '启用', type: 'success' },
|
||||||
|
disabled: { label: '停用', type: 'info' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const DATA_SCOPE_MAP = {
|
||||||
|
all: { label: '全部', type: 'success' },
|
||||||
|
dept: { label: '本部门', type: 'info' },
|
||||||
|
dept_and_child: { label: '本部门及以下', type: 'warning' },
|
||||||
|
self: { label: '本人', type: 'danger' },
|
||||||
|
custom: { label: '自定义', type: 'primary' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const columns: TableColumn[] = [
|
||||||
|
{ prop: 'name', label: '角色名称', minWidth: 140 },
|
||||||
|
{ prop: 'code', label: '角色编码', minWidth: 140 },
|
||||||
|
{ prop: 'dataScope', label: '数据权限', width: 130, align: 'center', type: 'tag', tagMap: DATA_SCOPE_MAP },
|
||||||
|
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||||
|
{ prop: 'remark', label: '备注', minWidth: 140 },
|
||||||
|
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', title: '角色名称', placeholder: '请输入名称' },
|
||||||
|
code: { type: 'string', title: '角色编码', placeholder: '请输入编码' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
title: '状态',
|
||||||
|
enum: ['enabled', 'disabled'],
|
||||||
|
enumNames: ['启用', '停用'],
|
||||||
|
placeholder: '全部',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: { type: 'string', title: '角色编码', placeholder: '如 super_admin', minLength: 2 },
|
||||||
|
name: { type: 'string', title: '角色名称', placeholder: '如 超级管理员' },
|
||||||
|
dataScope: {
|
||||||
|
type: 'string',
|
||||||
|
title: '数据权限',
|
||||||
|
enum: ['all', 'dept', 'dept_and_child', 'self', 'custom'],
|
||||||
|
enumNames: ['全部', '本部门', '本部门及以下', '本人', '自定义'],
|
||||||
|
default: 'all',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
title: '状态',
|
||||||
|
enum: ['enabled', 'disabled'],
|
||||||
|
enumNames: ['启用', '停用'],
|
||||||
|
default: 'enabled',
|
||||||
|
showOn: ['edit'],
|
||||||
|
},
|
||||||
|
remark: { type: 'string', title: '备注', format: 'textarea', placeholder: '请输入备注' },
|
||||||
|
},
|
||||||
|
required: ['code', 'name'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: CrudAction[] = [
|
||||||
|
'edit',
|
||||||
|
'delete',
|
||||||
|
{ label: '分配权限', type: 'warning', onClick: (row) => assignAction(row) },
|
||||||
|
]
|
||||||
|
|
||||||
|
function toListQuery(query: PageQuery): RoleListQuery {
|
||||||
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||||||
|
return {
|
||||||
|
page: query.page,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
name: str(query.name),
|
||||||
|
code: str(query.code),
|
||||||
|
status: str(query.status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = (query: PageQuery, signal?: AbortSignal) => listRoles(toListQuery(query), signal)
|
||||||
|
const create = (data: Record<string, any>) =>
|
||||||
|
createRole({ code: data.code, name: data.name, dataScope: data.dataScope, remark: data.remark })
|
||||||
|
const update = (id: string, data: Record<string, any>) =>
|
||||||
|
updateRole(id, { name: data.name, dataScope: data.dataScope, status: data.status, remark: data.remark })
|
||||||
|
const remove = (id: string) => deleteRole(id)
|
||||||
|
|
||||||
|
async function assignAction(row: Record<string, any>): Promise<void> {
|
||||||
|
const result = await showDialog({
|
||||||
|
component: AssignResourcesForm,
|
||||||
|
props: { roleCode: String(row.code) },
|
||||||
|
title: `分配权限(${row.name})`,
|
||||||
|
width: '520px',
|
||||||
|
})
|
||||||
|
if (result == null) return
|
||||||
|
await assignResources(String(row.code), result.resourceCodes)
|
||||||
|
ElMessage.success('权限已保存')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -36,13 +36,13 @@ import { useRequest } from '@/composables/useRequest'
|
|||||||
import {
|
import {
|
||||||
createUser,
|
createUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
listOrgs,
|
|
||||||
listUsers,
|
listUsers,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
updateUser,
|
updateUser,
|
||||||
type UserListQuery,
|
type UserListQuery,
|
||||||
type UserStatus,
|
type UserStatus,
|
||||||
} from '@/api/user'
|
} from '@/api/user'
|
||||||
|
import { listOrgs } from '@/api/org'
|
||||||
import ResetPasswordForm from './reset-password-form.vue'
|
import ResetPasswordForm from './reset-password-form.vue'
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
52
typed-router.d.ts
vendored
52
typed-router.d.ts
vendored
@ -58,6 +58,20 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/monitor/audit/': RouteRecordInfo<
|
||||||
|
'/monitor/audit/',
|
||||||
|
'/monitor/audit',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/monitor/login-log/': RouteRecordInfo<
|
||||||
|
'/monitor/login-log/',
|
||||||
|
'/monitor/login-log',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/register': RouteRecordInfo<
|
'/register': RouteRecordInfo<
|
||||||
'/register',
|
'/register',
|
||||||
'/register',
|
'/register',
|
||||||
@ -93,6 +107,20 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/system/role/': RouteRecordInfo<
|
||||||
|
'/system/role/',
|
||||||
|
'/system/role',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/system/role/assign-resources-form': RouteRecordInfo<
|
||||||
|
'/system/role/assign-resources-form',
|
||||||
|
'/system/role/assign-resources-form',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/system/user/': RouteRecordInfo<
|
'/system/user/': RouteRecordInfo<
|
||||||
'/system/user/',
|
'/system/user/',
|
||||||
'/system/user',
|
'/system/user',
|
||||||
@ -150,6 +178,18 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/monitor/audit/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/monitor/audit/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/monitor/login-log/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/monitor/login-log/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/register.vue': {
|
'src/pages/register.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/register'
|
| '/register'
|
||||||
@ -180,6 +220,18 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/system/role/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/system/role/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
|
'src/pages/system/role/assign-resources-form.vue': {
|
||||||
|
routes:
|
||||||
|
| '/system/role/assign-resources-form'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/system/user/index.vue': {
|
'src/pages/system/user/index.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/system/user/'
|
| '/system/user/'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user