Compare commits

..

No commits in common. "75560e0c4b7ec4c4eb533c7d678c8cff1dcc0970" and "340eaf5bb9917cac45dc3d89e44c5e5bd9415a80" have entirely different histories.

17 changed files with 368 additions and 597 deletions

View File

@ -59,14 +59,6 @@ pnpm sizecheck
- **UnoCSS 66.2.0** 原子化CSS - **UnoCSS 66.2.0** 原子化CSS
- **Alova 3.3.2** HTTP客户端 - **Alova 3.3.2** HTTP客户端
## 开发规范补充
### 字典数据使用约定
- **严禁写死常量**:凡是来源于数据库的枚举/状态类数据(启用/停用、成功/失败、性别、角色/菜单/文件服务等),必须通过后端字典接口 `listDataByType` 获取,不得在前端硬编码。
- **统一入口**:所有页面应使用 `useDict`/`useDictStore`、`DictTag` 与 `getSelectOptions` 等工具消费字典,确保新增字典值时页面自动生效。
- **缓存刷新**:对字典类型或字典数据进行增删改操作后,务必调用 `dictStore.invalidate` 失效前端缓存,确保其它模块能够获取最新字典信息。
- **新增字典类型**:若发现现有字典无法覆盖业务,需要先在后端补充字典类型及数据,再在前端按照上述方式接入,不得继续使用手工数组。
### 目录结构要点 ### 目录结构要点
``` ```
src/ src/
@ -175,7 +167,7 @@ src/
## ⚠️ 严格禁止事项 ## ⚠️ 严格禁止事项
**1. 消息提示** **1. 消息提示**
```text ```typescript
// ❌ 严禁使用Element Plus原生消息 // ❌ 严禁使用Element Plus原生消息
// ✅ 必须使用项目封装的消息函数 // ✅ 必须使用项目封装的消息函数
import { coiMsgError, coiMsgSuccess } from '@/utils/coi.ts' import { coiMsgError, coiMsgSuccess } from '@/utils/coi.ts'
@ -187,7 +179,7 @@ coiMsgError('操作失败')
``` ```
**2. 类型定义** **2. 类型定义**
```text ```typescript
// ❌ 严禁使用any类型 // ❌ 严禁使用any类型
const response: any = await getList() const response: any = await getList()
@ -196,7 +188,7 @@ const response: Result<ResponseVo[]> = await getList()
``` ```
**3. 数据访问** **3. 数据访问**
```text ```typescript
// ❌ 错误的数据访问 // ❌ 错误的数据访问
const data = response const data = response
@ -1237,10 +1229,74 @@ import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
### 标准按钮实现规范 ### 标准按钮实现规范
**✅ 正确的表格操作列按钮实现方式** **✅ 正确的表格操作列按钮实现方式**
- 编辑按钮:`type: 'primary'`、`size: 'small'`、`class: 'action-btn-primary'`,图标固定为 `IconParkOutlineEdit` ```typescript
- 删除按钮:使用 `NPopconfirm` 包裹 `type: 'error'`、`secondary: true`、`size: 'small'` 的按钮,图标用 `IconParkOutlineDelete`,确认文案统一为“确定删除此记录吗?”。 // 表格列定义 - 操作列
- 其它功能按钮:根据语义选择 `type: 'warning'`、`'info'` 等,并附加 `secondary: true` 与对应语义图标(如 `IconParkOutlineSetting`)。 {
- 操作列返回 `div.flex.items-center.justify-center.gap-2` 布局,按钮顺序遵循“编辑→删除→其他”。 title: '操作',
key: 'actions',
width: 280,
align: 'center',
fixed: 'right',
render: (row) => {
const buttons = []
// 编辑按钮 - 主要操作按钮
if (hasPermission('edit')) {
buttons.push(h(NButton, {
type: 'primary',
size: 'small',
class: 'action-btn-primary',
onClick: () => handleEdit(row),
}, {
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
default: () => h(IconParkOutlineEdit)
}),
default: () => '编辑',
}))
}
// 删除按钮 - 危险操作按钮
if (hasPermission('delete')) {
buttons.push(h(NPopconfirm, {
onPositiveClick: () => handleDelete(row.id),
negativeText: '取消',
positiveText: '确定',
}, {
default: () => '确定删除此记录吗?',
trigger: () => h(NButton, {
type: 'error',
secondary: true,
size: 'small',
class: 'action-btn-secondary action-btn-danger',
}, {
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
default: () => h(IconParkOutlineDelete),
}),
default: () => '删除',
}),
}))
}
// 其他功能按钮 - 辅助操作按钮
if (hasPermission('other')) {
buttons.push(h(NButton, {
type: 'warning',
secondary: true,
size: 'small',
class: 'action-btn-secondary action-btn-warning',
onClick: () => handleOtherAction(row),
}, {
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
default: () => h(IconParkOutlineSetting),
}),
default: () => '设置',
}))
}
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
},
}
```
### 按钮样式核心标准 ### 按钮样式核心标准
@ -1325,7 +1381,7 @@ const buttons = []
buttons.push(h(NButton, { buttons.push(h(NButton, {
type: 'primary', type: 'primary',
size: 'small', size: 'small',
class: 'action-btn-primary', class: 'action-btn-primary'
// ... 其他属性 // ... 其他属性
})) }))
@ -1336,7 +1392,7 @@ buttons.push(h(NPopconfirm, {
type: 'error', type: 'error',
secondary: true, secondary: true,
size: 'small', size: 'small',
class: 'action-btn-secondary action-btn-danger', class: 'action-btn-secondary action-btn-danger'
// ... 其他属性 // ... 其他属性
}) })
})) }))
@ -1346,7 +1402,7 @@ buttons.push(h(NButton, {
type: 'info', type: 'info',
secondary: true, secondary: true,
size: 'small', size: 'small',
class: 'action-btn-secondary action-btn-info', class: 'action-btn-secondary action-btn-info'
// ... 其他属性 // ... 其他属性
})) }))
``` ```

View File

@ -1,71 +0,0 @@
<template>
<n-tag
v-if="option"
:type="tagType"
:style="tagStyle"
:size="size"
:bordered="!option.dictColor"
class="dict-tag"
>
<slot :option="option" :label="option.dictLabel">
{{ option.dictLabel }}
</slot>
</n-tag>
<span v-else class="dict-tag__placeholder">
<slot name="placeholder" :value="value">
{{ fallbackLabel }}
</slot>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TagProps } from 'naive-ui'
import { useDict } from '@/hooks'
type DictValue = string | number | boolean | null | undefined
interface DictTagProps {
dictType: string
value: DictValue
size?: NonNullable<TagProps['size']>
fallbackLabel?: string
}
const props = withDefaults(defineProps<DictTagProps>(), {
size: 'small',
fallbackLabel: '-',
})
const { getDictOption, getDictLabel } = useDict(computed(() => [props.dictType]))
const option = computed(() => getDictOption(props.dictType, props.value))
const tagType = computed<TagProps['type']>(() => {
if (!option.value)
return 'default'
return option.value.dictColor ? 'default' : (option.value.dictTag as TagProps['type']) ?? 'default'
})
const tagStyle = computed(() => {
if (!option.value?.dictColor)
return undefined
return {
borderColor: option.value.dictColor,
backgroundColor: option.value.dictColor,
color: '#fff',
} satisfies Record<string, string>
})
const fallbackLabel = computed(() => getDictLabel(props.dictType, props.value, props.fallbackLabel))
const value = computed(() => props.value)
</script>
<style scoped>
.dict-tag__placeholder {
color: var(--n-text-color);
}
</style>

View File

@ -1,3 +1,2 @@
export * from './useBoolean' export * from './useBoolean'
export * from './usePermission' export * from './usePermission'
export * from './useDict'

View File

@ -1,75 +0,0 @@
import type { MaybeRef } from 'vue'
import { computed, unref, watch } from 'vue'
import { toSelectOptions } from '@/utils/dict'
import { useDictStore } from '@/store'
import type { DictDataOption } from '@/service/api/system/dict'
interface UseDictOptions {
/** 是否在创建时立即加载,默认为 true */
immediate?: boolean
/** 监听类型变化时是否强制刷新 */
force?: boolean
}
export function useDict(dictTypes: MaybeRef<string[] | undefined>, options: UseDictOptions = {}) {
const dictStore = useDictStore()
const normalizedTypes = computed(() => {
const value = unref(dictTypes) ?? []
return value.filter((item): item is string => Boolean(item))
})
const load = async (force = false) => {
const types = normalizedTypes.value
if (!types.length)
return
await dictStore.fetchDicts(types, force)
}
watch(
normalizedTypes,
(types) => {
if (!types.length)
return
void dictStore.fetchDicts(types, options.force ?? false)
},
{ immediate: options.immediate ?? true },
)
const dictOptions = computed<Record<string, DictDataOption[]>>(() => {
const result: Record<string, DictDataOption[]> = {}
normalizedTypes.value.forEach((type) => {
result[type] = dictStore.getDictOptions(type)
})
return result
})
const isLoading = computed(() => normalizedTypes.value.some(type => dictStore.isLoading(type)))
const getDictLabel = (dictType: string, value: unknown, fallback?: string) =>
dictStore.getDictLabel(dictType, value, fallback)
const getDictOption = (dictType: string, value: unknown) =>
dictStore.getDictOption(dictType, value)
const getSelectOptions = (dictType: string) => toSelectOptions(dictStore.getDictOptions(dictType))
const reload = async (targetTypes?: string[]) => {
const types = targetTypes && targetTypes.length ? targetTypes : normalizedTypes.value
if (!types.length)
return
await dictStore.fetchDicts(types, true)
}
return {
dictOptions,
isLoading,
load,
reload,
getDictLabel,
getDictOption,
getSelectOptions,
}
}

View File

@ -1,102 +0,0 @@
import { getDictDataByType } from '@/service/api/system/dict'
import type { DictDataOption } from '@/service/api/system/dict'
type DictValue = string | number | boolean | null | undefined
export const useDictStore = defineStore('dict-store', () => {
const dictMap = ref<Record<string, DictDataOption[]>>({})
const loadingMap = ref<Record<string, boolean>>({})
const pendingMap: Record<string, Promise<DictDataOption[]>> = {}
function getDictOptions(dictType: string) {
return dictMap.value[dictType] ?? []
}
function getDictOption(dictType: string, value: DictValue) {
if (value === undefined || value === null)
return undefined
const target = String(value)
return getDictOptions(dictType).find(option => option.dictValue === target)
}
function getDictLabel(dictType: string, value: DictValue, fallback?: string) {
const option = getDictOption(dictType, value)
if (option)
return option.dictLabel
if (fallback !== undefined)
return fallback
if (value === undefined || value === null || value === '')
return ''
return String(value)
}
async function fetchDict(dictType: string, force = false) {
if (!dictType)
return [] as DictDataOption[]
if (!force && dictMap.value[dictType])
return dictMap.value[dictType]
if (!force && pendingMap[dictType])
return pendingMap[dictType]
const promise = (async () => {
loadingMap.value[dictType] = true
try {
const { isSuccess, data } = await getDictDataByType(dictType)
if (isSuccess && Array.isArray(data))
dictMap.value[dictType] = data
else if (!dictMap.value[dictType])
dictMap.value[dictType] = []
return dictMap.value[dictType]
}
finally {
loadingMap.value[dictType] = false
delete pendingMap[dictType]
}
})()
if (!force)
pendingMap[dictType] = promise
return promise
}
async function fetchDicts(dictTypes: string[], force = false) {
const uniqueTypes = Array.from(new Set(dictTypes.filter(Boolean)))
await Promise.all(uniqueTypes.map(type => fetchDict(type, force)))
}
function isLoading(dictType: string) {
return Boolean(loadingMap.value[dictType])
}
function invalidate(dictType?: string) {
if (dictType) {
delete dictMap.value[dictType]
delete loadingMap.value[dictType]
delete pendingMap[dictType]
}
else {
dictMap.value = {}
loadingMap.value = {}
Object.keys(pendingMap).forEach(key => delete pendingMap[key])
}
}
return {
dictMap,
fetchDict,
fetchDicts,
getDictOptions,
getDictOption,
getDictLabel,
isLoading,
invalidate,
}
})

View File

@ -5,7 +5,6 @@ export * from './app/index'
export * from './auth' export * from './auth'
export * from './router' export * from './router'
export * from './tab' export * from './tab'
export * from './dict'
// 安装pinia全局状态库 // 安装pinia全局状态库
export function installPinia(app: App) { export function installPinia(app: App) {

View File

@ -1,65 +0,0 @@
import type { DictDataOption } from '@/service/api/system/dict'
export type DictValue = string | number | boolean | null | undefined
/**
* Naive UI Select
*/
export function toSelectOptions(dictOptions: DictDataOption[] = []) {
return dictOptions.map(option => ({
label: option.dictLabel,
value: option.dictValue,
}))
}
/**
*
*/
export function findDictOption(dictOptions: DictDataOption[] = [], value: DictValue) {
if (value === undefined || value === null)
return undefined
const target = String(value)
return dictOptions.find(option => option.dictValue === target)
}
/**
*
*/
export function findDictLabel(dictOptions: DictDataOption[] = [], value: DictValue, fallback?: string) {
const target = findDictOption(dictOptions, value)
if (target)
return target.dictLabel
if (fallback !== undefined)
return fallback
if (value === undefined || value === null || value === '')
return ''
return String(value)
}
/**
*
*/
export function findDictColor(dictOptions: DictDataOption[] = [], value: DictValue) {
const target = findDictOption(dictOptions, value)
if (!target)
return undefined
return {
tag: target.dictTag,
color: target.dictColor,
label: target.dictLabel,
}
}
/**
* -> Map便
*/
export function createDictMap(dictOptions: DictDataOption[] = []) {
const map = new Map<string, DictDataOption>()
dictOptions.forEach(option => map.set(option.dictValue, option))
return map
}

View File

@ -97,7 +97,13 @@
<div class="flex justify-between items-center py-2"> <div class="flex justify-between items-center py-2">
<span class="text-gray-600">状态</span> <span class="text-gray-600">状态</span>
<DictTag dict-type="sys_switch_status" :value="personalData.userStatus || '0'" /> <n-tag
:type="(personalData.userStatus || '0') === '0' ? 'info' : 'error'"
:style="(personalData.userStatus || '0') === '0' ? { backgroundColor: '#6366f1', color: 'white', border: 'none' } : {}"
size="small"
>
{{ getStatusText(personalData.userStatus || '0') }}
</n-tag>
</div> </div>
<div class="flex justify-between items-center py-2"> <div class="flex justify-between items-center py-2">
@ -254,14 +260,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { getPersonalData, updateBasicData, updatePassword, uploadAvatar } from '@/service/api/personal' import { getPersonalData, updateBasicData, updatePassword, uploadAvatar } from '@/service/api/personal'
import type { PersonalDataVo, UpdatePasswordBo, UpdatePersonalBo } from '@/service/api/personal' import type { PersonalDataVo, UpdatePasswordBo, UpdatePersonalBo } from '@/service/api/personal'
import { serviceConfig } from '@/../service.config' import { serviceConfig } from '@/../service.config'
import DictTag from '@/components/common/DictTag.vue'
import { useDict } from '@/hooks'
// //
const authStore = useAuthStore() const authStore = useAuthStore()
@ -339,12 +343,29 @@ const passwordRules = {
], ],
} }
const { getSelectOptions, getDictLabel } = useDict(['sys_user_sex', 'sys_switch_status']) //
const genderOptions = [
{ label: '男', value: '1' },
{ label: '女', value: '2' },
{ label: '未知', value: '3' },
]
const genderOptions = computed(() => getSelectOptions('sys_user_sex')) //
const statusOptions = [
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]
//
function getGenderText(sex: string) { function getGenderText(sex: string) {
return getDictLabel('sys_user_sex', sex, '未知') const option = genderOptions.find(item => item.value === sex)
return option?.label || '未知'
}
//
function getStatusText(status: string) {
const option = statusOptions.find(item => item.value === status)
return option?.label || '未知'
} }
// //

View File

@ -327,8 +327,7 @@ import { NButton, NIcon, NInputNumber, NPopconfirm, NSpace, NSwitch, NTag } from
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
import { useDictStore } from '@/store'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { import {
addDictData, addDictData,
@ -375,9 +374,6 @@ const currentDictType = ref<DictTypeOption | null>(null)
const dictTypeOptions = ref<{ label: string, value: string }[]>([]) const dictTypeOptions = ref<{ label: string, value: string }[]>([])
const isFromTypeClick = ref(false) const isFromTypeClick = ref(false)
const dictStore = useDictStore()
const { getSelectOptions, getDictLabel } = useDict(['sys_switch_status', 'sys_tag_type'])
// //
const searchForm = reactive<DictDataSearchForm>({ const searchForm = reactive<DictDataSearchForm>({
dictType: '', dictType: '',
@ -418,8 +414,18 @@ function handlePageSizeChange(pageSize: number) {
} }
// //
const statusOptions = computed(() => getSelectOptions('sys_switch_status')) const statusOptions = [
const tagOptions = computed(() => getSelectOptions('sys_tag_type')) { label: '正常', value: DictStatus.NORMAL },
{ label: '停用', value: DictStatus.DISABLED },
]
//
const tagOptions = [
{ label: 'primary', value: DictTag.PRIMARY },
{ label: 'success', value: DictTag.SUCCESS },
{ label: 'info', value: DictTag.INFO },
{ label: 'warning', value: DictTag.WARNING },
]
// //
const rules: FormRules = { const rules: FormRules = {
@ -544,7 +550,7 @@ const columns: DataTableColumns<DictDataVo> = [
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要将字典数据「${row.dictLabel}」状态切换为「${getDictLabel('sys_switch_status', row.dictStatus === DictStatus.NORMAL ? '1' : '0')}」吗?`, default: () => `确定要${row.dictStatus === DictStatus.NORMAL ? '停用' : '启用'}字典数据「${row.dictLabel}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.dictStatus === DictStatus.NORMAL, value: row.dictStatus === DictStatus.NORMAL,
size: 'small', size: 'small',
@ -816,7 +822,6 @@ async function handleDelete(row: DictDataVo) {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess('删除成功') coiMsgSuccess('删除成功')
dictStore.invalidate(row.dictType)
getTableData() getTableData()
} }
else { else {
@ -844,8 +849,6 @@ async function handleBatchDelete() {
coiMsgSuccess('批量删除成功') coiMsgSuccess('批量删除成功')
checkedRowKeys.value = [] checkedRowKeys.value = []
selectedRows.value = [] selectedRows.value = []
if (searchForm.dictType)
dictStore.invalidate(searchForm.dictType)
getTableData() getTableData()
} }
else { else {
@ -865,9 +868,7 @@ async function handleStatusChange(row: DictDataVo) {
const { isSuccess } = await updateDictDataStatus(row.dictId, newStatus) const { isSuccess } = await updateDictDataStatus(row.dictId, newStatus)
if (isSuccess) { if (isSuccess) {
const statusLabel = getDictLabel('sys_switch_status', newStatus, newStatus === DictStatus.NORMAL ? '启用' : '停用') coiMsgSuccess('状态修改成功')
coiMsgSuccess(`状态已更新为「${statusLabel}`)
dictStore.invalidate(row.dictType)
getTableData() getTableData()
} }
else { else {
@ -887,7 +888,6 @@ async function handleSyncCache() {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess('缓存同步成功') coiMsgSuccess('缓存同步成功')
dictStore.invalidate()
} }
else { else {
coiMsgError('缓存同步失败') coiMsgError('缓存同步失败')
@ -936,8 +936,6 @@ async function handleSubmit() {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess(isEdit.value ? '修改成功' : '新增成功') coiMsgSuccess(isEdit.value ? '修改成功' : '新增成功')
formDialogRef.value?.coiClose() formDialogRef.value?.coiClose()
if (submitData.dictType)
dictStore.invalidate(submitData.dictType)
getTableData() getTableData()
} }
else { else {

View File

@ -251,8 +251,7 @@ import { NButton, NIcon, NPopconfirm, NSpace, NSwitch } from 'naive-ui'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
import { useDictStore } from '@/store'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { import {
addDictType, addDictType,
@ -277,7 +276,6 @@ import IconParkOutlineEdit from '~icons/icon-park-outline/edit'
// //
const router = useRouter() const router = useRouter()
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const { getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
// //
const loading = ref(false) const loading = ref(false)
@ -289,7 +287,6 @@ const formRef = ref<FormInst>()
const formDialogRef = ref() const formDialogRef = ref()
const isEdit = ref(false) const isEdit = ref(false)
const modalTitle = ref('') const modalTitle = ref('')
const dictStore = useDictStore()
// //
const searchForm = reactive<DictTypeSearchForm>({ const searchForm = reactive<DictTypeSearchForm>({
@ -327,7 +324,10 @@ function handlePageSizeChange(pageSize: number) {
} }
// //
const statusOptions = computed(() => getSelectOptions('sys_switch_status')) const statusOptions = [
{ label: '正常', value: DictStatus.NORMAL },
{ label: '停用', value: DictStatus.DISABLED },
]
// //
const rules: FormRules = { const rules: FormRules = {
@ -411,12 +411,10 @@ const columns: DataTableColumns<DictTypeVo> = [
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要将字典类型「${row.dictName}」状态切换为「${getDictLabel('sys_switch_status', row.dictStatus === DictStatus.NORMAL ? '1' : '0')}」吗?`, default: () => `确定要${row.dictStatus === DictStatus.NORMAL ? '停用' : '启用'}字典类型「${row.dictName}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.dictStatus === DictStatus.NORMAL, value: row.dictStatus === DictStatus.NORMAL,
size: 'small', size: 'small',
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
}), }),
}), }),
]) ])
@ -597,7 +595,6 @@ async function handleDelete(row: DictTypeVo) {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess('删除成功') coiMsgSuccess('删除成功')
dictStore.invalidate()
getTableData() getTableData()
} }
else { else {
@ -625,7 +622,6 @@ async function handleBatchDelete() {
coiMsgSuccess('批量删除成功') coiMsgSuccess('批量删除成功')
checkedRowKeys.value = [] checkedRowKeys.value = []
selectedRows.value = [] selectedRows.value = []
dictStore.invalidate()
getTableData() getTableData()
} }
else { else {
@ -645,9 +641,7 @@ async function handleStatusChange(row: DictTypeVo) {
const { isSuccess } = await updateDictTypeStatus(row.dictId, newStatus) const { isSuccess } = await updateDictTypeStatus(row.dictId, newStatus)
if (isSuccess) { if (isSuccess) {
const statusLabel = getDictLabel('sys_switch_status', newStatus, newStatus === DictStatus.NORMAL ? '启用' : '停用') coiMsgSuccess('状态修改成功')
coiMsgSuccess(`状态已更新为「${statusLabel}`)
dictStore.invalidate()
getTableData() getTableData()
} }
else { else {
@ -667,7 +661,6 @@ async function handleSyncCache() {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess('缓存同步成功') coiMsgSuccess('缓存同步成功')
dictStore.invalidate()
} }
else { else {
coiMsgError('缓存同步失败') coiMsgError('缓存同步失败')
@ -711,7 +704,6 @@ async function handleSubmit() {
if (isSuccess) { if (isSuccess) {
coiMsgSuccess(isEdit.value ? '修改成功' : '新增成功') coiMsgSuccess(isEdit.value ? '修改成功' : '新增成功')
formDialogRef.value?.coiClose() formDialogRef.value?.coiClose()
dictStore.invalidate()
getTableData() getTableData()
} }
else { else {

View File

@ -72,7 +72,7 @@
v-model:value="searchForm.fileService" v-model:value="searchForm.fileService"
placeholder="请选择存储类型" placeholder="请选择存储类型"
clearable clearable
:options="getSelectOptions('sys_file_service')" :options="FILE_SERVICE_DB_OPTIONS"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -215,7 +215,11 @@
<n-select <n-select
v-model:value="uploadForm.fileService" v-model:value="uploadForm.fileService"
placeholder="请选择文件服务" placeholder="请选择文件服务"
:options="getSelectOptions('sys_file_service')" :options="[
{ label: 'LOCAL', value: 'LOCAL' },
{ label: 'OSS', value: 'OSS' },
{ label: 'MINIO', value: 'MINIO' },
]"
/> />
</n-form-item> </n-form-item>
@ -280,8 +284,7 @@ import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import CoiImageViewer from '@/components/common/CoiImageViewer.vue' import CoiImageViewer from '@/components/common/CoiImageViewer.vue'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import DictTag from '@/components/common/DictTag.vue' import { usePermission } from '@/hooks/usePermission'
import { useDict, usePermission } from '@/hooks'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { import {
batchDeleteSysFiles, batchDeleteSysFiles,
@ -292,6 +295,7 @@ import {
import type { SysFileQueryBo, SysFileSearchForm, SysFileVo } from '@/service/api/system/file' import type { SysFileQueryBo, SysFileSearchForm, SysFileVo } from '@/service/api/system/file'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineDownload from '~icons/icon-park-outline/download' import IconParkOutlineDownload from '~icons/icon-park-outline/download'
import { FILE_SERVICE_DB_OPTIONS } from '@/service/api/system/file/types'
// //
const StarIcon = () => h('span', { class: 'text-yellow-500' }, '✨') const StarIcon = () => h('span', { class: 'text-yellow-500' }, '✨')
@ -317,27 +321,6 @@ const fileCategories = [
// //
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const { getSelectOptions } = useDict(['sys_file_service'])
const storageValueToType: Record<string, string> = {
1: 'LOCAL',
2: 'MINIO',
3: 'OSS',
}
const storageTypeToValue: Record<string, string> = {
LOCAL: '1',
MINIO: '2',
OSS: '3',
}
function mapServiceValueToType(value: string | null | undefined) {
const str = value != null ? String(value) : '1'
if (storageValueToType[str])
return storageValueToType[str]
const upper = str.toUpperCase()
return storageValueToType[storageTypeToValue[upper] ?? '1'] ?? 'LOCAL'
}
// //
const searchFormRef = ref<FormInst>() const searchFormRef = ref<FormInst>()
@ -393,7 +376,7 @@ const isConfirmDisabled = computed(() => {
// //
const uploadForm = ref({ const uploadForm = ref({
fileService: '1', fileService: 'LOCAL',
filePath: '', filePath: '',
}) })
@ -498,8 +481,19 @@ const columns: DataTableColumns<SysFileVo> = [
title: '文件服务类型', title: '文件服务类型',
key: 'fileService', key: 'fileService',
align: 'center', align: 'center',
width: 140, width: 120,
render: row => h(DictTag, { dictType: 'sys_file_service', value: row.fileService }), render: (row) => {
const serviceMap: Record<string, { type: 'success' | 'info' | 'warning', text: string }> = {
1: { type: 'success', text: '本地存储' },
2: { type: 'info', text: 'MinIO存储' },
3: { type: 'warning', text: '阿里云OSS' },
}
const config = serviceMap[row.fileService] || { type: 'info', text: '未知' }
return h(NTag, {
type: config.type,
size: 'small',
}, { default: () => config.text })
},
}, },
{ {
title: '创建时间', title: '创建时间',
@ -787,8 +781,7 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
onProgress({ percent: 10 }) onProgress({ percent: 10 })
// API // API
const storageType = mapServiceValueToType(uploadForm.value.fileService) const result = await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
const result = await uploadFile(fileObj, folderName, 2, '-1', storageType)
// //
if (result.isSuccess === false) { if (result.isSuccess === false) {

View File

@ -51,7 +51,10 @@
v-model:value="searchForm.loginStatus" v-model:value="searchForm.loginStatus"
placeholder="请选择登录状态" placeholder="请选择登录状态"
clearable clearable
:options="loginStatusOptions" :options="[
{ label: '成功', value: '0' },
{ label: '失败', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -193,13 +196,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, nextTick, onMounted, ref } from 'vue' import { h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
batchDeleteLoginLog, batchDeleteLoginLog,
deleteLoginLog, deleteLoginLog,
@ -208,21 +210,10 @@ import {
import type { LoginLogVo } from '@/service/api/system/loginlog' import type { LoginLogVo } from '@/service/api/system/loginlog'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
// //
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const { dictOptions } = useDict(['sys_common_status'])
const loginStatusOptions = computed(() => {
const options = dictOptions.sys_common_status || []
return options
.filter(option => option.dictValue === '1' || option.dictValue === '2')
.map(option => ({
label: option.dictLabel,
value: option.dictValue === '1' ? '0' : '1',
}))
})
// //
interface LoginLogSearchForm { interface LoginLogSearchForm {
@ -329,9 +320,17 @@ const columns: DataTableColumns<LoginLogVo> = [
{ {
title: '登录状态', title: '登录状态',
key: 'loginStatus', key: 'loginStatus',
width: 110, width: 100,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.loginStatus === '0' ? '1' : '2' }), render: (row) => {
const isSuccess = row.loginStatus === '0'
return h(NTag, {
type: isSuccess ? 'success' : 'error',
size: 'small',
}, {
default: () => isSuccess ? '操作成功' : '操作失败',
})
},
}, },
{ {
title: '登录信息', title: '登录信息',

View File

@ -40,7 +40,10 @@
v-model:value="searchForm.menuStatus" v-model:value="searchForm.menuStatus"
placeholder="请选择菜单状态" placeholder="请选择菜单状态"
clearable clearable
:options="getSelectOptions('sys_switch_status')" :options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -232,7 +235,7 @@
:style="formData.parentId === '0' ? { color: themeColors.primary, fontWeight: '500' } : {}" :style="formData.parentId === '0' ? { color: themeColors.primary, fontWeight: '500' } : {}"
@click="handleSelectParent('0', '最顶级菜单')" @click="handleSelectParent('0', '最顶级菜单')"
> >
<NRadio <n-radio
:checked="formData.parentId === '0'" :checked="formData.parentId === '0'"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -260,7 +263,7 @@
@click="handleSelectParent(menu.value, menu.label)" @click="handleSelectParent(menu.value, menu.label)"
@mouseenter="handleMenuHover(menu)" @mouseenter="handleMenuHover(menu)"
> >
<NRadio <n-radio
:checked="formData.parentId === menu.value" :checked="formData.parentId === menu.value"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -300,7 +303,7 @@
:style="formData.parentId === submenu.value ? { color: themeColors.primary, fontWeight: '500' } : {}" :style="formData.parentId === submenu.value ? { color: themeColors.primary, fontWeight: '500' } : {}"
@click="handleSelectParent(submenu.value, submenu.label)" @click="handleSelectParent(submenu.value, submenu.label)"
> >
<NRadio <n-radio
:checked="formData.parentId === submenu.value" :checked="formData.parentId === submenu.value"
style="pointer-events: none;" style="pointer-events: none;"
/> />
@ -322,18 +325,20 @@
<!-- 菜单类型 --> <!-- 菜单类型 -->
<n-form-item label="菜单类型" path="menuType" class="mb-2"> <n-form-item label="菜单类型" path="menuType" class="mb-2">
<NRadioGroup <n-radio-group
v-model:value="formData.menuType" v-model:value="formData.menuType"
@update:value="handleMenuTypeChange" @update:value="handleMenuTypeChange"
> >
<NRadio <n-radio value="1">
v-for="item in dictOptions.sys_menu_type || []" 目录
:key="item.dictValue" </n-radio>
:value="item.dictValue" <n-radio value="2">
> 菜单
{{ item.dictLabel }} </n-radio>
</NRadio> <n-radio value="3">
</NRadioGroup> 按钮
</n-radio>
</n-radio-group>
</n-form-item> </n-form-item>
<!-- 菜单图标 --> <!-- 菜单图标 -->
@ -387,14 +392,14 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否隐藏" path="isHide"> <n-form-item label="是否隐藏" path="isHide">
<NRadioGroup v-model:value="formData.isHide"> <n-radio-group v-model:value="formData.isHide">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -433,14 +438,14 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否缓存" path="isKeepAlive"> <n-form-item label="是否缓存" path="isKeepAlive">
<NRadioGroup v-model:value="formData.isKeepAlive"> <n-radio-group v-model:value="formData.isKeepAlive">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -457,26 +462,26 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否展开" path="isSpread"> <n-form-item label="是否展开" path="isSpread">
<NRadioGroup v-model:value="formData.isSpread"> <n-radio-group v-model:value="formData.isSpread">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
<n-form-item label="是否固钉" path="isAffix"> <n-form-item label="是否固钉" path="isAffix">
<NRadioGroup v-model:value="formData.isAffix"> <n-radio-group v-model:value="formData.isAffix">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
@ -487,26 +492,26 @@
<n-grid :cols="2" :x-gap="10" class="mb-2"> <n-grid :cols="2" :x-gap="10" class="mb-2">
<n-grid-item> <n-grid-item>
<n-form-item label="是否展开" path="isSpread"> <n-form-item label="是否展开" path="isSpread">
<NRadioGroup v-model:value="formData.isSpread"> <n-radio-group v-model:value="formData.isSpread">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
<n-form-item label="是否固钉" path="isAffix"> <n-form-item label="是否固钉" path="isAffix">
<NRadioGroup v-model:value="formData.isAffix"> <n-radio-group v-model:value="formData.isAffix">
<NRadio value="0"> <n-radio value="0">
</NRadio> </n-radio>
<NRadio value="1"> <n-radio value="1">
</NRadio> </n-radio>
</NRadioGroup> </n-radio-group>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
@ -530,7 +535,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, nextTick, onMounted, ref } from 'vue' import { computed, h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui' import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, NSpace, NSwitch, NTag } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineEdit from '~icons/icon-park-outline/edit' import IconParkOutlineEdit from '~icons/icon-park-outline/edit'
import IconParkOutlinePlus from '~icons/icon-park-outline/plus' import IconParkOutlinePlus from '~icons/icon-park-outline/plus'
@ -538,9 +543,8 @@ import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiIcon from '@/components/common/CoiIcon.vue' import CoiIcon from '@/components/common/CoiIcon.vue'
import IconSelect from '@/components/common/IconSelect.vue' import IconSelect from '@/components/common/IconSelect.vue'
import DictTag from '@/components/common/DictTag.vue'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { import {
addMenu, addMenu,
@ -583,11 +587,6 @@ const selectedRows = ref<MenuVo[]>([])
const expandedKeys = ref<string[]>([]) const expandedKeys = ref<string[]>([])
const isAllExpanded = ref(true) const isAllExpanded = ref(true)
const { dictOptions, getSelectOptions, getDictLabel } = useDict([
'sys_menu_type',
'sys_switch_status',
])
// //
const menuDialogRef = ref() const menuDialogRef = ref()
const formRef = ref<FormInst>() const formRef = ref<FormInst>()
@ -717,7 +716,15 @@ const columns: DataTableColumns<MenuVo> = [
key: 'menuType', key: 'menuType',
width: 100, width: 100,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_menu_type', value: row.menuType }), render: (row) => {
const typeMap = {
1: { label: '目录', color: 'primary' },
2: { label: '菜单', color: 'info' },
3: { label: '按钮', color: 'warning' },
}
const type = typeMap[Number(row.menuType) as keyof typeof typeMap]
return h(NTag, { type: type.color }, { default: () => type.label })
},
}, },
{ {
title: '展开/折叠', title: '展开/折叠',
@ -811,8 +818,6 @@ const columns: DataTableColumns<MenuVo> = [
render: (row) => { render: (row) => {
return h(NSwitch, { return h(NSwitch, {
value: row.menuStatus === '0', value: row.menuStatus === '0',
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
onUpdateValue: value => handleStatusChange(row, value ? '0' : '1'), onUpdateValue: value => handleStatusChange(row, value ? '0' : '1'),
}) })
}, },
@ -1310,8 +1315,7 @@ async function handleStatusChange(menu: MenuVo, status: string) {
// //
updateMenuStatusInData(tableData.value, menu.menuId, status) updateMenuStatusInData(tableData.value, menu.menuId, status)
const statusLabel = getDictLabel('sys_switch_status', status, status === '0' ? '启用' : '停用') coiMsgSuccess('状态修改成功')
coiMsgSuccess(`状态已更新为「${statusLabel}`)
} }
catch { catch {
coiMsgError('状态修改失败') coiMsgError('状态修改失败')

View File

@ -40,7 +40,10 @@
v-model:value="searchForm.operStatus" v-model:value="searchForm.operStatus"
placeholder="请选择操作状态" placeholder="请选择操作状态"
clearable clearable
:options="operStatusOptions" :options="[
{ label: '操作成功', value: '0' },
{ label: '操作失败', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -230,7 +233,9 @@
{{ currentLogDetail.costTime || '-' }} {{ currentLogDetail.costTime || '-' }}
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="操作状态"> <n-descriptions-item label="操作状态">
<DictTag dict-type="sys_common_status" :value="currentLogDetail.operStatus === '0' ? '1' : '2'" /> <NTag :type="currentLogDetail.operStatus === '0' ? 'success' : 'error'" size="small">
{{ currentLogDetail.operStatus === '0' ? '操作成功' : '操作失败' }}
</NTag>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item v-if="currentLogDetail.operParam" label="请求参数" :span="2"> <n-descriptions-item v-if="currentLogDetail.operParam" label="请求参数" :span="2">
<div class="json-container"> <div class="json-container">
@ -250,7 +255,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, h, nextTick, onMounted, ref } from 'vue' import { h, nextTick, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace, NTag } from 'naive-ui'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
@ -258,7 +263,6 @@ import IconParkOutlinePreviewOpen from '~icons/icon-park-outline/preview-open'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
batchDeleteOperLog, batchDeleteOperLog,
clearOperLog, clearOperLog,
@ -269,21 +273,10 @@ import {
import type { OperLogSearchForm, OperLogVo } from '@/service/api/system/operlog' import type { OperLogSearchForm, OperLogVo } from '@/service/api/system/operlog'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
const { dictOptions } = useDict(['sys_oper_type', 'sys_common_status'])
const operStatusOptions = computed(() => {
const options = dictOptions.sys_common_status || []
return options
.filter(option => option.dictValue === '1' || option.dictValue === '2')
.map(option => ({
label: option.dictLabel,
value: option.dictValue === '1' ? '0' : '1',
}))
})
// //
const loading = ref(false) const loading = ref(false)
@ -346,9 +339,16 @@ const columns: DataTableColumns<OperLogVo> = [
{ {
title: '操作类型', title: '操作类型',
key: 'operType', key: 'operType',
width: 110, width: 90,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_oper_type', value: row.operType }), render: (row) => {
return h(NTag, {
type: 'primary',
size: 'small',
}, {
default: () => row.operType,
})
},
}, },
{ {
title: '操作人员[登录名/用户名]', title: '操作人员[登录名/用户名]',
@ -419,7 +419,15 @@ const columns: DataTableColumns<OperLogVo> = [
key: 'operStatus', key: 'operStatus',
width: 90, width: 90,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.operStatus === '0' ? '1' : '2' }), render: (row) => {
const isSuccess = row.operStatus === '0'
return h(NTag, {
type: isSuccess ? 'success' : 'error',
size: 'small',
}, {
default: () => isSuccess ? '操作成功' : '操作失败',
})
},
}, },
{ {
title: '操作时间', title: '操作时间',

View File

@ -72,7 +72,7 @@
v-model:value="searchForm.pictureService" v-model:value="searchForm.pictureService"
placeholder="请选择存储类型" placeholder="请选择存储类型"
clearable clearable
:options="getSelectOptions('sys_file_service')" :options="PICTURE_SERVICE_OPTIONS"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -258,9 +258,7 @@
<div><span class="text-gray-400">新名称:</span> {{ picture.newName }}</div> <div><span class="text-gray-400">新名称:</span> {{ picture.newName }}</div>
<div><span class="text-gray-400">文件大小:</span> {{ picture.pictureSize }}</div> <div><span class="text-gray-400">文件大小:</span> {{ picture.pictureSize }}</div>
<div><span class="text-gray-400">文件后缀:</span> {{ picture.pictureSuffix?.toUpperCase() }}</div> <div><span class="text-gray-400">文件后缀:</span> {{ picture.pictureSuffix?.toUpperCase() }}</div>
<div class="flex items-center gap-1"> <div><span class="text-gray-400">服务类型:</span> {{ getPictureServiceText(picture.pictureService) }}</div>
<span class="text-gray-400">服务类型:</span> <DictTag dict-type="sys_file_service" :value="mapStorageTypeToValue(picture.pictureService)" />
</div>
<div><span class="text-gray-400">创建时间:</span> {{ picture.createTime ? new Date(picture.createTime).toLocaleString() : '--' }}</div> <div><span class="text-gray-400">创建时间:</span> {{ picture.createTime ? new Date(picture.createTime).toLocaleString() : '--' }}</div>
<div><span class="text-gray-400">创建者:</span> {{ picture.createBy || '--' }}</div> <div><span class="text-gray-400">创建者:</span> {{ picture.createBy || '--' }}</div>
</div> </div>
@ -350,7 +348,11 @@
<n-select <n-select
v-model:value="uploadForm.pictureService" v-model:value="uploadForm.pictureService"
placeholder="请选择服务类型" placeholder="请选择服务类型"
:options="getSelectOptions('sys_file_service')" :options="[
{ label: 'LOCAL', value: 'LOCAL' },
{ label: 'OSS', value: 'OSS' },
{ label: 'MINIO', value: 'MINIO' },
]"
/> />
</n-form-item> </n-form-item>
@ -415,13 +417,13 @@ import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import CoiImageViewer from '@/components/common/CoiImageViewer.vue' import CoiImageViewer from '@/components/common/CoiImageViewer.vue'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import DictTag from '@/components/common/DictTag.vue' import { usePermission } from '@/hooks/usePermission'
import { useDict, usePermission } from '@/hooks'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { import {
batchDeleteSysPictures, batchDeleteSysPictures,
deleteSysPicture, deleteSysPicture,
getSysPictureList, getSysPictureList,
PICTURE_SERVICE_OPTIONS,
PICTURE_TYPE_OPTIONS, PICTURE_TYPE_OPTIONS,
uploadPicture, uploadPicture,
} from '@/service/api/system/picture' } from '@/service/api/system/picture'
@ -456,35 +458,6 @@ const uploadPictureTypeOptions = PICTURE_TYPE_OPTIONS.filter(option => option.va
// //
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const { getSelectOptions } = useDict(['sys_file_service'])
const storageValueToType: Record<string, string> = {
1: 'LOCAL',
2: 'MINIO',
3: 'OSS',
}
const storageTypeToValue: Record<string, string> = {
LOCAL: '1',
MINIO: '2',
OSS: '3',
}
function mapServiceValueToType(value: string | null | undefined) {
const str = value != null ? String(value) : '1'
if (storageValueToType[str])
return storageValueToType[str]
const upper = str.toUpperCase()
return storageValueToType[storageTypeToValue[upper] ?? '1'] ?? 'LOCAL'
}
function mapStorageTypeToValue(type: string | null | undefined) {
const str = type != null ? String(type) : '1'
if (storageValueToType[str])
return str
const upper = str.toUpperCase()
return storageTypeToValue[upper] ?? '1'
}
// //
const searchFormRef = ref<FormInst>() const searchFormRef = ref<FormInst>()
@ -541,7 +514,7 @@ const isConfirmDisabled = computed(() => {
// //
const uploadForm = ref({ const uploadForm = ref({
pictureService: '1', pictureService: 'LOCAL',
pictureType: '9', // pictureType: '9', //
picturePath: '', picturePath: '',
}) })
@ -649,9 +622,20 @@ const columns: DataTableColumns<SysPictureVo> = [
{ {
title: '服务类型', title: '服务类型',
key: 'pictureService', key: 'pictureService',
width: 130, width: 120,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_file_service', value: mapStorageTypeToValue(row.pictureService) }), render: (row) => {
const serviceMap: Record<string, { type: 'success' | 'info' | 'warning', text: string }> = {
1: { type: 'success', text: '本地存储' },
2: { type: 'info', text: 'MinIO存储' },
3: { type: 'warning', text: '阿里云OSS' },
}
const config = serviceMap[row.pictureService] || { type: 'info', text: '未知' }
return h(NTag, {
type: config.type,
size: 'small',
}, { default: () => config.text })
},
}, },
{ {
title: '创建时间', title: '创建时间',
@ -853,12 +837,21 @@ function handleDownload(row: SysPictureVo) {
} }
// //
function getPictureServiceText(serviceType: string): string {
const serviceMap: Record<string, string> = {
1: '本地存储',
2: 'MinIO存储',
3: '阿里云OSS',
}
return serviceMap[serviceType] || '未知'
}
// //
// //
function handleUpload() { function handleUpload() {
uploadForm.value = { uploadForm.value = {
pictureService: '1', pictureService: 'LOCAL',
pictureType: selectedCategory.value === '0' ? '9' : selectedCategory.value, // pictureType: selectedCategory.value === '0' ? '9' : selectedCategory.value, //
picturePath: '', picturePath: '',
} }
@ -929,8 +922,7 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
onProgress({ percent: 10 }) onProgress({ percent: 10 })
// API - 使 // API - 使
const storageType = mapServiceValueToType(uploadForm.value.pictureService) const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, storageType)
// //
if (result.isSuccess === false) { if (result.isSuccess === false) {

View File

@ -61,7 +61,10 @@
v-model:value="searchForm.roleStatus" v-model:value="searchForm.roleStatus"
placeholder="请选择角色状态" placeholder="请选择角色状态"
clearable clearable
:options="getSelectOptions('sys_switch_status')" :options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -266,15 +269,14 @@
<n-grid :cols="2" :x-gap="10"> <n-grid :cols="2" :x-gap="10">
<n-grid-item> <n-grid-item>
<n-form-item label="角色状态" path="roleStatus" class="mb-2"> <n-form-item label="角色状态" path="roleStatus" class="mb-2">
<NRadioGroup v-model:value="formData.roleStatus"> <n-select
<NRadio v-model:value="formData.roleStatus"
v-for="item in dictOptions.sys_switch_status || []" placeholder="请选择角色状态"
:key="item.dictValue" :options="[
:value="item.dictValue" { label: '启用', value: '0' },
> { label: '停用', value: '1' },
{{ item.dictLabel }} ]"
</NRadio> />
</NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
<n-grid-item> <n-grid-item>
@ -418,14 +420,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onMounted, ref, watch } from 'vue' import { h, nextTick, onMounted, ref, watch } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, NSpace, NSwitch, NTag, NTree } from 'naive-ui' import { NButton, NIcon, NPopconfirm, NSpace, NSwitch, NTag, NTree } from 'naive-ui'
import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one' import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineKey from '~icons/icon-park-outline/key' import IconParkOutlineKey from '~icons/icon-park-outline/key'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
addRole, addRole,
batchDeleteRoles, batchDeleteRoles,
@ -445,11 +446,10 @@ import {
import type { MenuPermissionData, MenuVo } from '@/service/api/system/menu' import type { MenuPermissionData, MenuVo } from '@/service/api/system/menu'
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
const { dictOptions, getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
// //
const loading = ref(false) const loading = ref(false)
@ -546,27 +546,28 @@ const columns: DataTableColumns<RoleVo> = [
key: 'roleCode', key: 'roleCode',
width: 150, width: 150,
align: 'center', align: 'center',
render: row => h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode }), render: (row) => {
return h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode })
},
}, },
{ {
title: '角色状态', title: '角色状态',
key: 'roleStatus', key: 'roleStatus',
width: 120, width: 100,
align: 'center', align: 'center',
render: (row) => { render: (row) => {
return h('div', { class: 'flex items-center justify-center gap-2' }, [ return h('div', { class: 'flex items-center justify-center' }, [
h(DictTag, { dictType: 'sys_switch_status', value: row.roleStatus }),
h(NPopconfirm, { h(NPopconfirm, {
onPositiveClick: () => handleToggleStatus(row), onPositiveClick: () => handleToggleStatus(row),
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要将角色「${row.roleName}」状态切换为「${getDictLabel('sys_switch_status', row.roleStatus === '0' ? '1' : '0')}」吗?`, default: () => `确定要${row.roleStatus === '0' ? '停用' : '启用'}角色「${row.roleName}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.roleStatus === '0', value: row.roleStatus === '0',
size: 'small', size: 'small',
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'), checkedChildren: '启用',
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'), uncheckedChildren: '停用',
loading: false, loading: false,
}), }),
}), }),
@ -965,7 +966,7 @@ async function handleBatchDelete() {
async function handleToggleStatus(role: RoleVo) { async function handleToggleStatus(role: RoleVo) {
try { try {
const newStatus = role.roleStatus === '0' ? '1' : '0' const newStatus = role.roleStatus === '0' ? '1' : '0'
const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用') const statusText = newStatus === '0' ? '启用' : '停用'
const { isSuccess } = await updateRoleStatus(role.roleId, newStatus) const { isSuccess } = await updateRoleStatus(role.roleId, newStatus)
if (isSuccess) { if (isSuccess) {

View File

@ -72,7 +72,10 @@
v-model:value="searchForm.userStatus" v-model:value="searchForm.userStatus"
placeholder="请选择用户状态" placeholder="请选择用户状态"
clearable clearable
:options="getSelectOptions('sys_switch_status')" :options="[
{ label: '启用', value: '0' },
{ label: '停用', value: '1' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -310,7 +313,11 @@
<n-select <n-select
v-model:value="formData.userType" v-model:value="formData.userType"
placeholder="请选择用户类型" placeholder="请选择用户类型"
:options="getSelectOptions('sys_user_type')" :options="[
{ label: '系统用户', value: '1' },
{ label: '注册用户', value: '2' },
{ label: '微信用户', value: '3' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -320,7 +327,11 @@
<n-select <n-select
v-model:value="formData.sex" v-model:value="formData.sex"
placeholder="请选择性别" placeholder="请选择性别"
:options="getSelectOptions('sys_user_sex')" :options="[
{ label: '男', value: '1' },
{ label: '女', value: '2' },
{ label: '未知', value: '3' },
]"
/> />
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -356,15 +367,14 @@
<n-grid-item> <n-grid-item>
<n-form-item label="用户状态" path="userStatus" class="mb-2"> <n-form-item label="用户状态" path="userStatus" class="mb-2">
<NRadioGroup v-model:value="formData.userStatus"> <n-radio-group v-model:value="formData.userStatus">
<NRadio <n-radio value="0">
v-for="item in dictOptions.sys_switch_status || []" 启用
:key="item.dictValue" </n-radio>
:value="item.dictValue" <n-radio value="1">
> 停用
{{ item.dictLabel }} </n-radio>
</NRadio> </n-radio-group>
</NRadioGroup>
</n-form-item> </n-form-item>
</n-grid-item> </n-grid-item>
@ -575,8 +585,11 @@
{{ currentAvatarUser?.avatar ? '用户头像' : '默认头像' }} {{ currentAvatarUser?.avatar ? '用户头像' : '默认头像' }}
</p> </p>
<div class="flex items-center justify-center gap-4 text-xs text-gray-400"> <div class="flex items-center justify-center gap-4 text-xs text-gray-400">
<span>用户类型: {{ getDictLabel('sys_user_type', currentAvatarUser?.userType, '--') }}</span> <span>用户类型: {{
<span>状态: {{ getDictLabel('sys_switch_status', currentAvatarUser?.userStatus, '--') }}</span> currentAvatarUser?.userType === '1' ? '系统用户'
: currentAvatarUser?.userType === '2' ? '注册用户' : '微信用户'
}}</span>
<span>状态: {{ currentAvatarUser?.userStatus === '0' ? '启用' : '停用' }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -691,7 +704,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import type { DataTableColumns, FormInst } from 'naive-ui' import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NCheckbox, NDropdown, NIcon, NPopconfirm, NProgress, NRadio, NRadioGroup, NSpace, NSwitch, NTag, NUpload, NUploadDragger } from 'naive-ui' import { NButton, NCheckbox, NDropdown, NIcon, NPopconfirm, NProgress, NSpace, NSwitch, NTag, NUpload, NUploadDragger } from 'naive-ui'
import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one' import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one'
import IconParkOutlineDelete from '~icons/icon-park-outline/delete' import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh' import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh'
@ -702,7 +715,6 @@ import IconParkOutlineFileCodeOne from '~icons/icon-park-outline/file-code-one'
import CoiDialog from '@/components/common/CoiDialog.vue' import CoiDialog from '@/components/common/CoiDialog.vue'
import CoiEmpty from '@/components/common/CoiEmpty.vue' import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiPagination from '@/components/common/CoiPagination.vue' import CoiPagination from '@/components/common/CoiPagination.vue'
import DictTag from '@/components/common/DictTag.vue'
import { import {
addUser, addUser,
batchDeleteUsers, batchDeleteUsers,
@ -727,7 +739,7 @@ import {
import type { RoleVo } from '@/service/api/system/role' import type { RoleVo } from '@/service/api/system/role'
import { coiMsgBox, coiMsgError, coiMsgInfo, coiMsgSuccess, coiMsgWarning } from '@/utils/coi' import { coiMsgBox, coiMsgError, coiMsgInfo, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import { PERMISSIONS } from '@/constants/permissions' import { PERMISSIONS } from '@/constants/permissions'
import { useDict, usePermission } from '@/hooks' import { usePermission } from '@/hooks/usePermission'
// //
const { hasButton } = usePermission() const { hasButton } = usePermission()
@ -778,12 +790,6 @@ const uploadProgress = ref(0)
const progressInterval = ref<NodeJS.Timeout | null>(null) const progressInterval = ref<NodeJS.Timeout | null>(null)
const createdBlobUrls = ref<string[]>([]) const createdBlobUrls = ref<string[]>([])
const { dictOptions, getSelectOptions, getDictLabel } = useDict([
'sys_user_type',
'sys_user_sex',
'sys_switch_status',
])
// //
const pagination = ref({ const pagination = ref({
page: 1, page: 1,
@ -950,14 +956,33 @@ const columns: DataTableColumns<UserVo> = [
key: 'userType', key: 'userType',
width: 100, width: 100,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_user_type', value: row.userType }), render: (row) => {
const typeMap: Record<string, { label: string, type: any }> = {
1: { label: '系统用户', type: 'primary' },
2: { label: '注册用户', type: 'info' },
3: { label: '微信用户', type: 'warning' },
}
const config = typeMap[row.userType] || { label: '未知', type: 'default' }
return h(NTag, { type: config.type, size: 'small' }, { default: () => config.label })
},
}, },
{ {
title: '性别', title: '性别',
key: 'sex', key: 'sex',
width: 80, width: 80,
align: 'center', align: 'center',
render: row => h(DictTag, { dictType: 'sys_user_sex', value: row.sex }), render: (row) => {
const sexMap: Record<string, { label: string, icon: string, color: string }> = {
1: { label: '男', icon: '♂', color: 'text-blue-500' },
2: { label: '女', icon: '♀', color: 'text-pink-500' },
3: { label: '未知', icon: '?', color: 'text-gray-400' },
}
const config = sexMap[row.sex || '3']
return h('div', { class: `flex items-center justify-center gap-1 ${config.color}` }, [
h('span', { class: 'text-lg' }, config.icon),
h('span', { class: 'text-xs' }, config.label),
])
},
}, },
{ {
title: '用户状态', title: '用户状态',
@ -965,21 +990,18 @@ const columns: DataTableColumns<UserVo> = [
width: 100, width: 100,
align: 'center', align: 'center',
render: (row) => { render: (row) => {
const nextStatus = row.userStatus === '0' ? '1' : '0'
const enableLabel = getDictLabel('sys_switch_status', '0', '启用')
const disableLabel = getDictLabel('sys_switch_status', '1', '停用')
return h('div', { class: 'flex items-center justify-center' }, [ return h('div', { class: 'flex items-center justify-center' }, [
h(NPopconfirm, { h(NPopconfirm, {
onPositiveClick: () => handleToggleStatus(row), onPositiveClick: () => handleToggleStatus(row),
negativeText: '取消', negativeText: '取消',
positiveText: '确定', positiveText: '确定',
}, { }, {
default: () => `确定要将用户「${row.userName}」状态切换为「${getDictLabel('sys_switch_status', nextStatus)}」吗?`, default: () => `确定要${row.userStatus === '0' ? '停用' : '启用'}用户「${row.userName}」吗?`,
trigger: () => h(NSwitch, { trigger: () => h(NSwitch, {
value: row.userStatus === '0', value: row.userStatus === '0',
size: 'small', size: 'small',
checkedChildren: enableLabel, checkedChildren: '启用',
uncheckedChildren: disableLabel, uncheckedChildren: '停用',
loading: false, loading: false,
}), }),
}), }),
@ -1335,7 +1357,7 @@ async function handleBatchDelete() {
async function handleToggleStatus(user: UserVo) { async function handleToggleStatus(user: UserVo) {
try { try {
const newStatus = user.userStatus === '0' ? '1' : '0' const newStatus = user.userStatus === '0' ? '1' : '0'
const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用') const statusText = newStatus === '0' ? '启用' : '停用'
const { isSuccess } = await updateUserStatus(user.userId, newStatus) const { isSuccess } = await updateUserStatus(user.userId, newStatus)
if (isSuccess) { if (isSuccess) {