feat: dict-driven rendering — useDict + component dictCode
- 新增 dict store 与 useDict Hook(登录后缓存、登出清空) - ty-form/ty-table 支持 dictCode 自动解析下拉与标签 - 用户/角色/资源/组织/租户/审计/登录日志页去硬编码映射 - 字典管理页内置标记、value 保护、标签色编辑 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c96151d2ae
commit
23e228783c
@ -6,6 +6,7 @@ export interface DictType {
|
||||
code: string
|
||||
name: string
|
||||
status: 'enabled' | 'disabled'
|
||||
builtIn: boolean
|
||||
remark: string | null
|
||||
createdAt: string
|
||||
}
|
||||
@ -18,6 +19,8 @@ export interface DictItem {
|
||||
value: string
|
||||
sort: number
|
||||
status: 'enabled' | 'disabled'
|
||||
builtIn: boolean
|
||||
colorType: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
@ -28,6 +31,26 @@ export interface PageResult<T> {
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
// ---- 聚合查询(GET /dicts) ----
|
||||
|
||||
export interface DictOption {
|
||||
label: string
|
||||
value: string
|
||||
colorType: string | null
|
||||
sort: number
|
||||
}
|
||||
|
||||
export interface DictEntry {
|
||||
code: string
|
||||
name: string
|
||||
items: DictOption[]
|
||||
}
|
||||
|
||||
/** 一次拉取全部启用字典(含字典项),供前端登录后缓存。 */
|
||||
export function listDicts(signal?: AbortSignal): Promise<{ list: DictEntry[] }> {
|
||||
return http.get<{ list: DictEntry[] }>('/dicts', { signal })
|
||||
}
|
||||
|
||||
// ---- 字典类型 ----
|
||||
|
||||
export function listDictTypes(page = 1, pageSize = 1000, signal?: AbortSignal): Promise<PageResult<DictType>> {
|
||||
@ -58,13 +81,14 @@ export function createDictItem(data: {
|
||||
label: string
|
||||
value: string
|
||||
sort?: number
|
||||
colorType?: string
|
||||
}): Promise<DictItem> {
|
||||
return http.post<DictItem>('/dict-items', data)
|
||||
}
|
||||
|
||||
export function updateDictItem(
|
||||
id: string,
|
||||
data: { label?: string; value?: string; sort?: number; status?: string },
|
||||
data: { label?: string; value?: string; sort?: number; status?: string; colorType?: string },
|
||||
): Promise<DictItem> {
|
||||
return http.patch<DictItem>(`/dict-items/${id}`, data)
|
||||
}
|
||||
|
||||
@ -107,6 +107,8 @@ const props = withDefaults(
|
||||
nameKey?: string
|
||||
/** 编辑模式下强制只读的字段。 */
|
||||
editReadonlyKeys?: string[]
|
||||
/** 编辑模式下按行计算只读字段(优先于 editReadonlyKeys)。 */
|
||||
getEditReadonlyKeys?: (row: Record<string, any>) => string[]
|
||||
/** 操作列动作(内置 'edit'/'delete' 或自定义动作)。 */
|
||||
actions?: CrudAction[]
|
||||
/** 操作列按钮超过该数量时折叠为「更多」。 */
|
||||
@ -197,7 +199,7 @@ async function openEdit(row: Record<string, any>): Promise<void> {
|
||||
schema: props.formSchema,
|
||||
modelValue: model,
|
||||
mode: 'edit',
|
||||
readonlyKeys: props.editReadonlyKeys,
|
||||
readonlyKeys: props.getEditReadonlyKeys ? props.getEditReadonlyKeys(row) : props.editReadonlyKeys,
|
||||
},
|
||||
title: `编辑${props.entityName}`,
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:prop="key"
|
||||
>
|
||||
<el-select
|
||||
v-if="prop.enum"
|
||||
v-if="prop.enum || prop.dictCode"
|
||||
v-model="modelValue[key]"
|
||||
:placeholder="prop.placeholder ?? '请选择'"
|
||||
:clearable="!isRequired(key)"
|
||||
@ -15,10 +15,10 @@
|
||||
:style="widgetStyle(prop)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(v, i) in prop.enum"
|
||||
:key="String(v)"
|
||||
:label="prop.enumNames?.[i] ?? String(v)"
|
||||
:value="v"
|
||||
v-for="opt in selectOptions(prop)"
|
||||
:key="String(opt.value)"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
@ -86,9 +86,12 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { JsonSchema, JsonSchemaProperty } from '@/types/crud'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
|
||||
defineOptions({ name: 'TyForm' })
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 表单 JSON Schema。 */
|
||||
@ -167,6 +170,17 @@ function isReadonly(key: string, prop: { readOnly?: boolean }): boolean {
|
||||
return prop.readOnly === true || props.readonlyKeys.includes(key)
|
||||
}
|
||||
|
||||
/** 下拉选项:dictCode 优先,其次 enum/enumNames。 */
|
||||
function selectOptions(prop: JsonSchemaProperty): { value: string | number | boolean; label: string }[] {
|
||||
if (prop.dictCode) {
|
||||
return (dictStore.dicts[prop.dictCode] ?? []).map((i) => ({ value: i.value, label: i.label }))
|
||||
}
|
||||
if (prop.enum) {
|
||||
return prop.enum.map((v, i) => ({ value: v, label: prop.enumNames?.[i] ?? String(v) }))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function widgetStyle(prop: JsonSchemaProperty): Record<string, string> {
|
||||
return { width: prop.width ?? (props.inline ? '200px' : '100%') }
|
||||
}
|
||||
|
||||
@ -23,10 +23,10 @@
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="col.type === 'tag'"
|
||||
:type="col.tagMap?.[String(row[col.prop])]?.type ?? 'info'"
|
||||
:type="tagInfo(col, row[col.prop]).type"
|
||||
size="small"
|
||||
>
|
||||
{{ col.tagMap?.[String(row[col.prop])]?.label ?? row[col.prop] }}
|
||||
{{ tagInfo(col, row[col.prop]).label }}
|
||||
</el-tag>
|
||||
<span v-else-if="col.type === 'datetime'">{{ formatDateTime(row[col.prop]) }}</span>
|
||||
<span v-else-if="col.formatter">{{ col.formatter(row[col.prop], row) }}</span>
|
||||
@ -58,10 +58,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { TableColumn } from '@/types/crud'
|
||||
import type { TableColumn, TagType } from '@/types/crud'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
|
||||
defineOptions({ name: 'TyTable' })
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 列配置。 */
|
||||
@ -125,6 +128,17 @@ function onSelectionChange(rows: Record<string, any>[]): void {
|
||||
emit('selection-change', rows)
|
||||
}
|
||||
|
||||
/** 标签渲染:dictCode 优先,其次 tagMap;无映射时回退原始值。 */
|
||||
function tagInfo(col: TableColumn, value: unknown): { label: string; type: TagType } {
|
||||
const key = String(value ?? '')
|
||||
if (col.dictCode) {
|
||||
const item = (dictStore.dicts[col.dictCode] ?? []).find((i) => i.value === key)
|
||||
if (item) return { label: item.label, type: (item.colorType ?? 'info') as TagType }
|
||||
return { label: key, type: 'info' }
|
||||
}
|
||||
return col.tagMap?.[key] ?? { label: key, type: 'info' }
|
||||
}
|
||||
|
||||
function formatDateTime(value: unknown): string {
|
||||
if (value == null || value === '') return '-'
|
||||
const d = typeof value === 'number' ? new Date(value) : new Date(String(value))
|
||||
|
||||
27
src/composables/useDict.ts
Normal file
27
src/composables/useDict.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { computed } from 'vue'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
import type { TagType } from '@/types/crud'
|
||||
|
||||
/**
|
||||
* 读取某个字典类型的字典项,返回三种视图:
|
||||
* - items: 响应式字典项数组(label/value/colorType/sort)
|
||||
* - tagMap: value → { label, type },供表格标签列使用
|
||||
* - options: { label, value }[],供下拉选择使用
|
||||
*/
|
||||
export function useDict(code: string) {
|
||||
const store = useDictStore()
|
||||
|
||||
const items = computed(() => store.dicts[code] ?? [])
|
||||
|
||||
const tagMap = computed(() => {
|
||||
const map: Record<string, { label: string; type: TagType }> = {}
|
||||
for (const it of items.value) {
|
||||
map[it.value] = { label: it.label, type: (it.colorType ?? 'info') as TagType }
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const options = computed(() => items.value.map((it) => ({ label: it.label, value: it.value })))
|
||||
|
||||
return { items, tagMap, options }
|
||||
}
|
||||
@ -73,6 +73,7 @@ import { useAppStore } from '@/stores/modules/app'
|
||||
import { HOME_PATH, useTabsStore } from '@/stores/modules/tabs'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import { useMenuStore } from '@/stores/modules/menu'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import type { LayoutSettings } from '@/config/layout'
|
||||
import type { ResourceNode } from '@/types/resource'
|
||||
@ -83,6 +84,7 @@ const app = useAppStore()
|
||||
const tabsStore = useTabsStore()
|
||||
const auth = useAuthStore()
|
||||
const menuStore = useMenuStore()
|
||||
const dictStore = useDictStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||
@ -92,9 +94,10 @@ const HEADER_HEIGHT = 56
|
||||
const menus = computed(() => menuStore.menus)
|
||||
const settingsOpen = ref(false)
|
||||
|
||||
// 登录后拉取菜单
|
||||
// 登录后拉取菜单与字典
|
||||
onMounted(() => {
|
||||
menuStore.fetch()
|
||||
dictStore.fetch()
|
||||
})
|
||||
|
||||
const activePath = computed(() => route.path)
|
||||
@ -219,6 +222,7 @@ function handleCloseAll(): void {
|
||||
async function handleLogout(): Promise<void> {
|
||||
await auth.logout()
|
||||
menuStore.reset()
|
||||
dictStore.reset()
|
||||
tabsStore.removeAll()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import { Env } from '@/utils/env.ts'
|
||||
import { installI18n } from '@/i18n'
|
||||
import { setAuthExpiredHandler } from '@/api/http'
|
||||
import { useMenuStore } from '@/stores/modules/menu'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
|
||||
const app = createApp(App)
|
||||
@ -19,6 +20,7 @@ installStore(app)
|
||||
// 会话过期(401 刷新失败)时:清状态 + 跳登录
|
||||
setAuthExpiredHandler(() => {
|
||||
useMenuStore(pinia).reset()
|
||||
useDictStore(pinia).reset()
|
||||
useTabsStore(pinia).removeAll()
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
router.push('/login')
|
||||
|
||||
@ -25,18 +25,13 @@ 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: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'login_status' },
|
||||
{ prop: 'ip', label: 'IP', width: 130 },
|
||||
{ prop: 'durationMs', label: '耗时(ms)', width: 90, align: 'center' },
|
||||
{ prop: 'createdAt', label: '时间', width: 160, type: 'datetime' },
|
||||
@ -50,8 +45,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['success', 'fail'],
|
||||
enumNames: ['成功', '失败'],
|
||||
dictCode: 'login_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
|
||||
@ -25,15 +25,10 @@ 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: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'login_status' },
|
||||
{ prop: 'message', label: '信息', minWidth: 160 },
|
||||
{ prop: 'userAgent', label: 'UA', minWidth: 200 },
|
||||
{ prop: 'createdAt', label: '登录时间', width: 160, type: 'datetime' },
|
||||
@ -46,8 +41,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['success', 'fail'],
|
||||
enumNames: ['成功', '失败'],
|
||||
dictCode: 'login_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
|
||||
@ -16,16 +16,19 @@
|
||||
@click="selectType(t)"
|
||||
>
|
||||
<div class="dict-page__type-item-row">
|
||||
<span class="dict-page__type-item-name">{{ t.name }}</span>
|
||||
<span class="dict-page__type-item-name">
|
||||
{{ t.name }}
|
||||
<el-tag v-if="t.builtIn" size="small" type="primary" effect="plain">内置</el-tag>
|
||||
</span>
|
||||
<div class="dict-page__type-item-actions" @click.stop>
|
||||
<el-button link type="primary" size="small" @click="openTypeEdit(t)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeType(t)">删除</el-button>
|
||||
<el-button v-if="!t.builtIn" link type="danger" size="small" @click="removeType(t)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dict-page__type-item-meta">
|
||||
<span class="dict-page__type-item-code">{{ t.code }}</span>
|
||||
<el-tag :type="t.status === 'enabled' ? 'success' : 'info'" size="small">
|
||||
{{ t.status === 'enabled' ? '启用' : '停用' }}
|
||||
<el-tag :type="commonStatusMap[t.status]?.type ?? 'info'" size="small">
|
||||
{{ commonStatusMap[t.status]?.label ?? t.status }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
@ -38,6 +41,7 @@
|
||||
<ty-crud
|
||||
v-if="selectedType"
|
||||
:key="selectedType.code"
|
||||
ref="itemCrudRef"
|
||||
entity-name="字典项"
|
||||
:columns="itemColumns"
|
||||
:search-schema="itemSearchSchema"
|
||||
@ -46,6 +50,8 @@
|
||||
:create="createItem"
|
||||
:update="updateItem"
|
||||
:remove="removeItem"
|
||||
:actions="itemActions"
|
||||
:get-edit-readonly-keys="(row) => (row.builtIn ? ['value'] : [])"
|
||||
id-key="id"
|
||||
name-key="label"
|
||||
/>
|
||||
@ -59,10 +65,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||
import TyForm from '@/components/ty-form.vue'
|
||||
import { showDialog } from '@/utils/dialog'
|
||||
import { useRequest } from '@/composables/useRequest'
|
||||
import { useDict } from '@/composables/useDict'
|
||||
import {
|
||||
createDictItem,
|
||||
createDictType,
|
||||
@ -75,10 +82,16 @@ import {
|
||||
type DictType,
|
||||
} from '@/api/dict'
|
||||
|
||||
const STATUS_MAP = {
|
||||
enabled: { label: '启用', type: 'success' },
|
||||
disabled: { label: '停用', type: 'info' },
|
||||
} as const
|
||||
const { tagMap: commonStatusMap } = useDict('common_status')
|
||||
|
||||
/** 标签色中文名(UI 调色板,非业务字典)。 */
|
||||
const COLOR_LABELS: Record<string, string> = {
|
||||
primary: '主色',
|
||||
success: '成功',
|
||||
warning: '警告',
|
||||
danger: '危险',
|
||||
info: '信息',
|
||||
}
|
||||
|
||||
// ---- 字典类型(左) ----
|
||||
const { data: typePage, refresh: refreshTypes } = useRequest((signal: AbortSignal) => listDictTypes(1, 1000, signal))
|
||||
@ -112,8 +125,7 @@ const typeFormSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
showOn: ['edit'],
|
||||
},
|
||||
@ -164,8 +176,10 @@ async function removeType(t: DictType): Promise<void> {
|
||||
const itemColumns: TableColumn[] = [
|
||||
{ prop: 'label', label: '标签', minWidth: 120 },
|
||||
{ prop: 'value', label: '值', minWidth: 120 },
|
||||
{ prop: 'sort', label: '排序', width: 80, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||
{ prop: 'colorType', label: '标签色', width: 90, align: 'center', formatter: (v) => (v ? COLOR_LABELS[String(v)] ?? String(v) : '—') },
|
||||
{ prop: 'builtIn', label: '内置', width: 70, align: 'center', formatter: (v) => (v ? '内置' : '') },
|
||||
{ prop: 'sort', label: '排序', width: 70, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||
]
|
||||
|
||||
@ -182,12 +196,18 @@ const itemFormSchema: JsonSchema = {
|
||||
properties: {
|
||||
label: { type: 'string', title: '标签', placeholder: '如 启用' },
|
||||
value: { type: 'string', title: '值', placeholder: '如 enabled' },
|
||||
colorType: {
|
||||
type: 'string',
|
||||
title: '标签色',
|
||||
enum: ['primary', 'success', 'warning', 'danger', 'info'],
|
||||
enumNames: ['主色', '成功', '警告', '危险', '信息'],
|
||||
placeholder: '默认灰',
|
||||
},
|
||||
sort: { type: 'integer', title: '排序', minimum: 0, default: 0 },
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
showOn: ['edit'],
|
||||
},
|
||||
@ -219,12 +239,33 @@ const createItem = (data: Record<string, any>) =>
|
||||
label: data.label,
|
||||
value: data.value,
|
||||
sort: data.sort ?? 0,
|
||||
colorType: data.colorType || undefined,
|
||||
})
|
||||
|
||||
const updateItem = (id: string, data: Record<string, any>) =>
|
||||
updateDictItem(id, { label: data.label, value: data.value, sort: data.sort ?? 0, status: data.status })
|
||||
updateDictItem(id, {
|
||||
label: data.label,
|
||||
value: data.value,
|
||||
sort: data.sort ?? 0,
|
||||
status: data.status,
|
||||
colorType: data.colorType || undefined,
|
||||
})
|
||||
|
||||
const removeItem = (id: string) => deleteDictItem(id)
|
||||
|
||||
const itemCrudRef = ref()
|
||||
|
||||
const itemActions: CrudAction[] = [
|
||||
'edit',
|
||||
{ label: '删除', type: 'danger', onClick: (row) => removeWithConfirm(row), show: (row) => !row.builtIn },
|
||||
]
|
||||
|
||||
async function removeWithConfirm(row: Record<string, any>): Promise<void> {
|
||||
await ElMessageBox.confirm(`确认删除字典项「${row.label}」?`, '提示', { type: 'warning' })
|
||||
await deleteDictItem(String(row.id))
|
||||
ElMessage.success('删除成功')
|
||||
itemCrudRef.value?.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -48,11 +48,6 @@ import { useRequest } from '@/composables/useRequest'
|
||||
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
||||
import { createOrg, deleteOrg, listOrgs, updateOrg, type Org } from '@/api/org'
|
||||
|
||||
const STATUS_MAP = {
|
||||
enabled: { label: '启用', type: 'success' },
|
||||
disabled: { label: '停用', type: 'info' },
|
||||
} as const
|
||||
|
||||
type OrgNode = TreeNode<Org>
|
||||
|
||||
const { data: orgList, refresh: refreshOrgs } = useRequest((signal: AbortSignal) => listOrgs(signal))
|
||||
@ -63,7 +58,7 @@ const columns: TableColumn[] = [
|
||||
{ prop: 'code', label: '组织编码', minWidth: 120 },
|
||||
{ prop: 'leader', label: '负责人', width: 100 },
|
||||
{ prop: 'sort', label: '排序', width: 70, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||
]
|
||||
|
||||
@ -75,8 +70,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
@ -136,8 +130,7 @@ function orgFormSchema(excludeCode?: string): JsonSchema {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
showOn: ['edit'],
|
||||
},
|
||||
|
||||
@ -48,17 +48,6 @@ import { useRequest } from '@/composables/useRequest'
|
||||
import { buildTree, collectDescendants, filterTree, type TreeNode } from '@/utils/tree'
|
||||
import { createResource, deleteResource, listResources, updateResource, type Resource, type ResourceUpsertData } from '@/api/resource'
|
||||
|
||||
const STATUS_MAP = {
|
||||
enabled: { label: '启用', type: 'success' },
|
||||
disabled: { label: '停用', type: 'info' },
|
||||
} as const
|
||||
|
||||
const TYPE_MAP = {
|
||||
menu: { label: '菜单', type: 'primary' },
|
||||
button: { label: '按钮', type: 'warning' },
|
||||
api: { label: '接口', type: 'info' },
|
||||
} as const
|
||||
|
||||
type ResourceNode = TreeNode<Resource>
|
||||
|
||||
const { data: resourceList, refresh: refreshResources } = useRequest((signal: AbortSignal) => listResources(signal))
|
||||
@ -77,11 +66,11 @@ function pathDisplay(_value: unknown, row: Record<string, unknown>): string {
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ prop: 'name', label: '资源名称', minWidth: 200 },
|
||||
{ prop: 'type', label: '类型', width: 80, align: 'center', type: 'tag', tagMap: TYPE_MAP },
|
||||
{ prop: 'type', label: '类型', width: 80, align: 'center', type: 'tag', dictCode: 'resource_type' },
|
||||
{ prop: 'code', label: '资源编码', minWidth: 160 },
|
||||
{ prop: 'path', label: '路由 / 接口', minWidth: 180, formatter: pathDisplay },
|
||||
{ prop: 'sort', label: '排序', width: 70, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||
]
|
||||
|
||||
const searchSchema: JsonSchema = {
|
||||
@ -92,15 +81,13 @@ const searchSchema: JsonSchema = {
|
||||
type: {
|
||||
type: 'string',
|
||||
title: '类型',
|
||||
enum: ['menu', 'button', 'api'],
|
||||
enumNames: ['菜单', '按钮', '接口'],
|
||||
dictCode: 'resource_type',
|
||||
placeholder: '全部',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
@ -164,8 +151,7 @@ function resourceFormSchema(excludeCode?: string): JsonSchema {
|
||||
type: {
|
||||
type: 'string',
|
||||
title: '资源类型',
|
||||
enum: ['menu', 'button', 'api'],
|
||||
enumNames: ['菜单', '按钮', '接口'],
|
||||
dictCode: 'resource_type',
|
||||
default: 'menu',
|
||||
},
|
||||
code: { type: 'string', title: '资源编码', placeholder: '如 system:user:create' },
|
||||
@ -185,8 +171,7 @@ function resourceFormSchema(excludeCode?: string): JsonSchema {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
showOn: ['edit'],
|
||||
},
|
||||
|
||||
@ -22,24 +22,11 @@ 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: 'dataScope', label: '数据权限', width: 130, align: 'center', type: 'tag', dictCode: 'data_scope' },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||
{ prop: 'remark', label: '备注', minWidth: 140 },
|
||||
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||
]
|
||||
@ -52,8 +39,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
@ -67,15 +53,13 @@ const formSchema: JsonSchema = {
|
||||
dataScope: {
|
||||
type: 'string',
|
||||
title: '数据权限',
|
||||
enum: ['all', 'dept', 'dept_and_child', 'self', 'custom'],
|
||||
enumNames: ['全部', '本部门', '本部门及以下', '本人', '自定义'],
|
||||
dictCode: 'data_scope',
|
||||
default: 'all',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
showOn: ['edit'],
|
||||
},
|
||||
|
||||
@ -18,15 +18,10 @@
|
||||
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||
import { createTenant, deleteTenant, listTenants, updateTenant, type TenantListQuery } from '@/api/tenant'
|
||||
|
||||
const STATUS_MAP = {
|
||||
enabled: { label: '启用', type: 'success' },
|
||||
disabled: { label: '停用', type: 'info' },
|
||||
} as const
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ prop: 'name', label: '租户名称', minWidth: 140 },
|
||||
{ prop: 'code', label: '租户编码', minWidth: 140 },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||
{ prop: 'remark', label: '备注', minWidth: 160 },
|
||||
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||
]
|
||||
@ -39,8 +34,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
@ -54,8 +48,7 @@ const formSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled'],
|
||||
enumNames: ['启用', '停用'],
|
||||
dictCode: 'common_status',
|
||||
default: 'enabled',
|
||||
},
|
||||
remark: { type: 'string', title: '备注', format: 'textarea', placeholder: '请输入备注' },
|
||||
|
||||
@ -45,12 +45,6 @@ import {
|
||||
import { listOrgs } from '@/api/org'
|
||||
import ResetPasswordForm from './reset-password-form.vue'
|
||||
|
||||
const STATUS_MAP = {
|
||||
enabled: { label: '启用', type: 'success' },
|
||||
disabled: { label: '停用', type: 'info' },
|
||||
locked: { label: '锁定', type: 'danger' },
|
||||
} as const
|
||||
|
||||
// 组织列表(下拉 + 名称回显)
|
||||
const { data: orgs } = useRequest((signal: AbortSignal) => listOrgs(signal))
|
||||
|
||||
@ -62,7 +56,7 @@ const columns = computed<TableColumn[]>(() => {
|
||||
{ prop: 'nickname', label: '昵称', minWidth: 100 },
|
||||
{ prop: 'email', label: '邮箱', minWidth: 160 },
|
||||
{ prop: 'phone', label: '手机', minWidth: 120 },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', tagMap: STATUS_MAP },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'user_status' },
|
||||
{ prop: 'orgCode', label: '所属组织', minWidth: 110, formatter: (v) => orgName(v) },
|
||||
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||
]
|
||||
@ -77,8 +71,7 @@ const searchSchema: JsonSchema = {
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled', 'locked'],
|
||||
enumNames: ['启用', '停用', '锁定'],
|
||||
dictCode: 'user_status',
|
||||
placeholder: '全部',
|
||||
},
|
||||
},
|
||||
@ -102,8 +95,7 @@ const formSchema = computed<JsonSchema>(() => ({
|
||||
status: {
|
||||
type: 'string',
|
||||
title: '状态',
|
||||
enum: ['enabled', 'disabled', 'locked'],
|
||||
enumNames: ['启用', '停用', '锁定'],
|
||||
dictCode: 'user_status',
|
||||
default: 'enabled',
|
||||
},
|
||||
orgCode: {
|
||||
|
||||
31
src/stores/modules/dict.ts
Normal file
31
src/stores/modules/dict.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { listDicts, type DictOption } from '@/api/dict'
|
||||
|
||||
/** 字典缓存(登录后一次性拉取全部启用字典,非持久化)。 */
|
||||
export const useDictStore = defineStore('dictStore', () => {
|
||||
const dicts = ref<Record<string, DictOption[]>>({})
|
||||
const loaded = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetch(): Promise<void> {
|
||||
if (loaded.value || loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listDicts()
|
||||
const map: Record<string, DictOption[]> = {}
|
||||
for (const d of res.list) map[d.code] = d.items
|
||||
dicts.value = map
|
||||
loaded.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
dicts.value = {}
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return { dicts, loaded, loading, fetch, reset }
|
||||
})
|
||||
@ -14,6 +14,8 @@ export interface JsonSchemaProperty {
|
||||
enum?: (string | number | boolean)[]
|
||||
/** 枚举显示名(与 enum 一一对应)。 */
|
||||
enumNames?: string[]
|
||||
/** 字典 code:声明后从字典解析下拉选项(优先于 enum/enumNames)。 */
|
||||
dictCode?: string
|
||||
default?: unknown
|
||||
/** 只读(禁用编辑)。 */
|
||||
readOnly?: boolean
|
||||
@ -56,6 +58,8 @@ export interface TableColumn {
|
||||
type?: 'text' | 'tag' | 'datetime'
|
||||
/** type=tag 时的值→{label,type} 映射。 */
|
||||
tagMap?: Record<string, { label: string; type: TagType }>
|
||||
/** 字典 code:type=tag 时按字典渲染标签(优先于 tagMap)。 */
|
||||
dictCode?: string
|
||||
/** 自定义格式化。 */
|
||||
formatter?: (value: unknown, row: Record<string, unknown>) => string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user