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:
liuyun 2026-10-11 14:29:28 +08:00
parent c96151d2ae
commit 23e228783c
17 changed files with 214 additions and 116 deletions

View File

@ -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)
}

View File

@ -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}`,
})

View File

@ -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%') }
}

View File

@ -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))

View 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 }
}

View File

@ -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')
}

View File

@ -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')

View File

@ -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: '全部',
},
},

View File

@ -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: '全部',
},
},

View File

@ -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">

View File

@ -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'],
},

View File

@ -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'],
},

View File

@ -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'],
},

View File

@ -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: '请输入备注' },

View File

@ -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: {

View 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 }
})

View File

@ -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
}