Compare commits
6 Commits
340eaf5bb9
...
75560e0c4b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75560e0c4b | ||
|
|
5355993a2a | ||
|
|
862ed051ed | ||
|
|
05b0bc1376 | ||
|
|
7c1bf63133 | ||
|
|
d50611c05b |
92
CLAUDE.md
92
CLAUDE.md
@ -59,6 +59,14 @@ 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/
|
||||||
@ -167,7 +175,7 @@ src/
|
|||||||
## ⚠️ 严格禁止事项
|
## ⚠️ 严格禁止事项
|
||||||
|
|
||||||
**1. 消息提示**
|
**1. 消息提示**
|
||||||
```typescript
|
```text
|
||||||
// ❌ 严禁使用Element Plus原生消息
|
// ❌ 严禁使用Element Plus原生消息
|
||||||
// ✅ 必须使用项目封装的消息函数
|
// ✅ 必须使用项目封装的消息函数
|
||||||
import { coiMsgError, coiMsgSuccess } from '@/utils/coi.ts'
|
import { coiMsgError, coiMsgSuccess } from '@/utils/coi.ts'
|
||||||
@ -179,7 +187,7 @@ coiMsgError('操作失败')
|
|||||||
```
|
```
|
||||||
|
|
||||||
**2. 类型定义**
|
**2. 类型定义**
|
||||||
```typescript
|
```text
|
||||||
// ❌ 严禁使用any类型
|
// ❌ 严禁使用any类型
|
||||||
const response: any = await getList()
|
const response: any = await getList()
|
||||||
|
|
||||||
@ -188,7 +196,7 @@ const response: Result<ResponseVo[]> = await getList()
|
|||||||
```
|
```
|
||||||
|
|
||||||
**3. 数据访问**
|
**3. 数据访问**
|
||||||
```typescript
|
```text
|
||||||
// ❌ 错误的数据访问
|
// ❌ 错误的数据访问
|
||||||
const data = response
|
const data = response
|
||||||
|
|
||||||
@ -1229,74 +1237,10 @@ import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
|||||||
### 标准按钮实现规范
|
### 标准按钮实现规范
|
||||||
|
|
||||||
**✅ 正确的表格操作列按钮实现方式**:
|
**✅ 正确的表格操作列按钮实现方式**:
|
||||||
```typescript
|
- 编辑按钮:`type: 'primary'`、`size: 'small'`、`class: 'action-btn-primary'`,图标固定为 `IconParkOutlineEdit`。
|
||||||
// 表格列定义 - 操作列
|
- 删除按钮:使用 `NPopconfirm` 包裹 `type: 'error'`、`secondary: true`、`size: 'small'` 的按钮,图标用 `IconParkOutlineDelete`,确认文案统一为“确定删除此记录吗?”。
|
||||||
{
|
- 其它功能按钮:根据语义选择 `type: 'warning'`、`'info'` 等,并附加 `secondary: true` 与对应语义图标(如 `IconParkOutlineSetting`)。
|
||||||
title: '操作',
|
- 操作列返回 `div.flex.items-center.justify-center.gap-2` 布局,按钮顺序遵循“编辑→删除→其他”。
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 按钮样式核心标准
|
### 按钮样式核心标准
|
||||||
|
|
||||||
@ -1381,7 +1325,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',
|
||||||
// ... 其他属性
|
// ... 其他属性
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -1392,7 +1336,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',
|
||||||
// ... 其他属性
|
// ... 其他属性
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
@ -1402,7 +1346,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',
|
||||||
// ... 其他属性
|
// ... 其他属性
|
||||||
}))
|
}))
|
||||||
```
|
```
|
||||||
|
|||||||
71
src/components/common/DictTag.vue
Normal file
71
src/components/common/DictTag.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<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>
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './useBoolean'
|
export * from './useBoolean'
|
||||||
export * from './usePermission'
|
export * from './usePermission'
|
||||||
|
export * from './useDict'
|
||||||
|
|||||||
75
src/hooks/useDict.ts
Normal file
75
src/hooks/useDict.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/store/dict.ts
Normal file
102
src/store/dict.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -5,6 +5,7 @@ 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) {
|
||||||
|
|||||||
65
src/utils/dict.ts
Normal file
65
src/utils/dict.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -97,13 +97,7 @@
|
|||||||
|
|
||||||
<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>
|
||||||
<n-tag
|
<DictTag dict-type="sys_switch_status" :value="personalData.userStatus || '0'" />
|
||||||
: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">
|
||||||
@ -260,12 +254,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, 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()
|
||||||
@ -343,29 +339,12 @@ 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) {
|
||||||
const option = genderOptions.find(item => item.value === sex)
|
return getDictLabel('sys_user_sex', sex, '未知')
|
||||||
return option?.label || '未知'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取状态显示文字
|
|
||||||
function getStatusText(status: string) {
|
|
||||||
const option = statusOptions.find(item => item.value === status)
|
|
||||||
return option?.label || '未知'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取角色名称列表
|
// 获取角色名称列表
|
||||||
|
|||||||
@ -327,7 +327,8 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
import { useDictStore } from '@/store'
|
||||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||||
import {
|
import {
|
||||||
addDictData,
|
addDictData,
|
||||||
@ -374,6 +375,9 @@ 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: '',
|
||||||
@ -414,18 +418,8 @@ function handlePageSizeChange(pageSize: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 状态选项
|
// 状态选项
|
||||||
const statusOptions = [
|
const statusOptions = computed(() => getSelectOptions('sys_switch_status'))
|
||||||
{ label: '正常', value: DictStatus.NORMAL },
|
const tagOptions = computed(() => getSelectOptions('sys_tag_type'))
|
||||||
{ 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 = {
|
||||||
@ -550,7 +544,7 @@ const columns: DataTableColumns<DictDataVo> = [
|
|||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
}, {
|
}, {
|
||||||
default: () => `确定要${row.dictStatus === DictStatus.NORMAL ? '停用' : '启用'}字典数据「${row.dictLabel}」吗?`,
|
default: () => `确定要将字典数据「${row.dictLabel}」状态切换为「${getDictLabel('sys_switch_status', row.dictStatus === DictStatus.NORMAL ? '1' : '0')}」吗?`,
|
||||||
trigger: () => h(NSwitch, {
|
trigger: () => h(NSwitch, {
|
||||||
value: row.dictStatus === DictStatus.NORMAL,
|
value: row.dictStatus === DictStatus.NORMAL,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
@ -822,6 +816,7 @@ async function handleDelete(row: DictDataVo) {
|
|||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('删除成功')
|
coiMsgSuccess('删除成功')
|
||||||
|
dictStore.invalidate(row.dictType)
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -849,6 +844,8 @@ async function handleBatchDelete() {
|
|||||||
coiMsgSuccess('批量删除成功')
|
coiMsgSuccess('批量删除成功')
|
||||||
checkedRowKeys.value = []
|
checkedRowKeys.value = []
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
|
if (searchForm.dictType)
|
||||||
|
dictStore.invalidate(searchForm.dictType)
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -868,7 +865,9 @@ async function handleStatusChange(row: DictDataVo) {
|
|||||||
const { isSuccess } = await updateDictDataStatus(row.dictId, newStatus)
|
const { isSuccess } = await updateDictDataStatus(row.dictId, newStatus)
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('状态修改成功')
|
const statusLabel = getDictLabel('sys_switch_status', newStatus, newStatus === DictStatus.NORMAL ? '启用' : '停用')
|
||||||
|
coiMsgSuccess(`状态已更新为「${statusLabel}」`)
|
||||||
|
dictStore.invalidate(row.dictType)
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -888,6 +887,7 @@ async function handleSyncCache() {
|
|||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('缓存同步成功')
|
coiMsgSuccess('缓存同步成功')
|
||||||
|
dictStore.invalidate()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
coiMsgError('缓存同步失败')
|
coiMsgError('缓存同步失败')
|
||||||
@ -936,6 +936,8 @@ 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 {
|
||||||
|
|||||||
@ -251,7 +251,8 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
import { useDictStore } from '@/store'
|
||||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||||
import {
|
import {
|
||||||
addDictType,
|
addDictType,
|
||||||
@ -276,6 +277,7 @@ 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)
|
||||||
@ -287,6 +289,7 @@ 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>({
|
||||||
@ -324,10 +327,7 @@ function handlePageSizeChange(pageSize: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 状态选项
|
// 状态选项
|
||||||
const statusOptions = [
|
const statusOptions = computed(() => getSelectOptions('sys_switch_status'))
|
||||||
{ label: '正常', value: DictStatus.NORMAL },
|
|
||||||
{ label: '停用', value: DictStatus.DISABLED },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
@ -411,10 +411,12 @@ const columns: DataTableColumns<DictTypeVo> = [
|
|||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
}, {
|
}, {
|
||||||
default: () => `确定要${row.dictStatus === DictStatus.NORMAL ? '停用' : '启用'}字典类型「${row.dictName}」吗?`,
|
default: () => `确定要将字典类型「${row.dictName}」状态切换为「${getDictLabel('sys_switch_status', row.dictStatus === DictStatus.NORMAL ? '1' : '0')}」吗?`,
|
||||||
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', '停用'),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
@ -595,6 +597,7 @@ async function handleDelete(row: DictTypeVo) {
|
|||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('删除成功')
|
coiMsgSuccess('删除成功')
|
||||||
|
dictStore.invalidate()
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -622,6 +625,7 @@ async function handleBatchDelete() {
|
|||||||
coiMsgSuccess('批量删除成功')
|
coiMsgSuccess('批量删除成功')
|
||||||
checkedRowKeys.value = []
|
checkedRowKeys.value = []
|
||||||
selectedRows.value = []
|
selectedRows.value = []
|
||||||
|
dictStore.invalidate()
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -641,7 +645,9 @@ async function handleStatusChange(row: DictTypeVo) {
|
|||||||
const { isSuccess } = await updateDictTypeStatus(row.dictId, newStatus)
|
const { isSuccess } = await updateDictTypeStatus(row.dictId, newStatus)
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('状态修改成功')
|
const statusLabel = getDictLabel('sys_switch_status', newStatus, newStatus === DictStatus.NORMAL ? '启用' : '停用')
|
||||||
|
coiMsgSuccess(`状态已更新为「${statusLabel}」`)
|
||||||
|
dictStore.invalidate()
|
||||||
getTableData()
|
getTableData()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -661,6 +667,7 @@ async function handleSyncCache() {
|
|||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
coiMsgSuccess('缓存同步成功')
|
coiMsgSuccess('缓存同步成功')
|
||||||
|
dictStore.invalidate()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
coiMsgError('缓存同步失败')
|
coiMsgError('缓存同步失败')
|
||||||
@ -704,6 +711,7 @@ 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 {
|
||||||
|
|||||||
@ -72,7 +72,7 @@
|
|||||||
v-model:value="searchForm.fileService"
|
v-model:value="searchForm.fileService"
|
||||||
placeholder="请选择存储类型"
|
placeholder="请选择存储类型"
|
||||||
clearable
|
clearable
|
||||||
:options="FILE_SERVICE_DB_OPTIONS"
|
:options="getSelectOptions('sys_file_service')"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -215,11 +215,7 @@
|
|||||||
<n-select
|
<n-select
|
||||||
v-model:value="uploadForm.fileService"
|
v-model:value="uploadForm.fileService"
|
||||||
placeholder="请选择文件服务"
|
placeholder="请选择文件服务"
|
||||||
:options="[
|
:options="getSelectOptions('sys_file_service')"
|
||||||
{ label: 'LOCAL', value: 'LOCAL' },
|
|
||||||
{ label: 'OSS', value: 'OSS' },
|
|
||||||
{ label: 'MINIO', value: 'MINIO' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
@ -284,7 +280,8 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import DictTag from '@/components/common/DictTag.vue'
|
||||||
|
import { useDict, usePermission } from '@/hooks'
|
||||||
import { PERMISSIONS } from '@/constants/permissions'
|
import { PERMISSIONS } from '@/constants/permissions'
|
||||||
import {
|
import {
|
||||||
batchDeleteSysFiles,
|
batchDeleteSysFiles,
|
||||||
@ -295,7 +292,6 @@ 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' }, '✨')
|
||||||
@ -321,6 +317,27 @@ 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>()
|
||||||
@ -376,7 +393,7 @@ const isConfirmDisabled = computed(() => {
|
|||||||
|
|
||||||
// 上传表单数据
|
// 上传表单数据
|
||||||
const uploadForm = ref({
|
const uploadForm = ref({
|
||||||
fileService: 'LOCAL',
|
fileService: '1',
|
||||||
filePath: '',
|
filePath: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -481,19 +498,8 @@ const columns: DataTableColumns<SysFileVo> = [
|
|||||||
title: '文件服务类型',
|
title: '文件服务类型',
|
||||||
key: 'fileService',
|
key: 'fileService',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 120,
|
width: 140,
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_file_service', value: row.fileService }),
|
||||||
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: '创建时间',
|
||||||
@ -781,7 +787,8 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
onProgress({ percent: 10 })
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
// 调用上传API,传递选择的存储类型
|
// 调用上传API,传递选择的存储类型
|
||||||
const result = await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
|
const storageType = mapServiceValueToType(uploadForm.value.fileService)
|
||||||
|
const result = await uploadFile(fileObj, folderName, 2, '-1', storageType)
|
||||||
|
|
||||||
// 检查业务逻辑是否成功
|
// 检查业务逻辑是否成功
|
||||||
if (result.isSuccess === false) {
|
if (result.isSuccess === false) {
|
||||||
|
|||||||
@ -51,10 +51,7 @@
|
|||||||
v-model:value="searchForm.loginStatus"
|
v-model:value="searchForm.loginStatus"
|
||||||
placeholder="请选择登录状态"
|
placeholder="请选择登录状态"
|
||||||
clearable
|
clearable
|
||||||
:options="[
|
:options="loginStatusOptions"
|
||||||
{ label: '成功', value: '0' },
|
|
||||||
{ label: '失败', value: '1' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -196,12 +193,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, nextTick, onMounted, ref } from 'vue'
|
import { computed, 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 } 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,
|
||||||
@ -210,10 +208,21 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
|
||||||
// 权限相关
|
// 权限相关
|
||||||
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 {
|
||||||
@ -320,17 +329,9 @@ const columns: DataTableColumns<LoginLogVo> = [
|
|||||||
{
|
{
|
||||||
title: '登录状态',
|
title: '登录状态',
|
||||||
key: 'loginStatus',
|
key: 'loginStatus',
|
||||||
width: 100,
|
width: 110,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.loginStatus === '0' ? '1' : '2' }),
|
||||||
const isSuccess = row.loginStatus === '0'
|
|
||||||
return h(NTag, {
|
|
||||||
type: isSuccess ? 'success' : 'error',
|
|
||||||
size: 'small',
|
|
||||||
}, {
|
|
||||||
default: () => isSuccess ? '操作成功' : '操作失败',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '登录信息',
|
title: '登录信息',
|
||||||
|
|||||||
@ -40,10 +40,7 @@
|
|||||||
v-model:value="searchForm.menuStatus"
|
v-model:value="searchForm.menuStatus"
|
||||||
placeholder="请选择菜单状态"
|
placeholder="请选择菜单状态"
|
||||||
clearable
|
clearable
|
||||||
:options="[
|
:options="getSelectOptions('sys_switch_status')"
|
||||||
{ label: '启用', value: '0' },
|
|
||||||
{ label: '停用', value: '1' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -235,7 +232,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', '最顶级菜单')"
|
||||||
>
|
>
|
||||||
<n-radio
|
<NRadio
|
||||||
:checked="formData.parentId === '0'"
|
:checked="formData.parentId === '0'"
|
||||||
style="pointer-events: none;"
|
style="pointer-events: none;"
|
||||||
/>
|
/>
|
||||||
@ -263,7 +260,7 @@
|
|||||||
@click="handleSelectParent(menu.value, menu.label)"
|
@click="handleSelectParent(menu.value, menu.label)"
|
||||||
@mouseenter="handleMenuHover(menu)"
|
@mouseenter="handleMenuHover(menu)"
|
||||||
>
|
>
|
||||||
<n-radio
|
<NRadio
|
||||||
:checked="formData.parentId === menu.value"
|
:checked="formData.parentId === menu.value"
|
||||||
style="pointer-events: none;"
|
style="pointer-events: none;"
|
||||||
/>
|
/>
|
||||||
@ -303,7 +300,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)"
|
||||||
>
|
>
|
||||||
<n-radio
|
<NRadio
|
||||||
:checked="formData.parentId === submenu.value"
|
:checked="formData.parentId === submenu.value"
|
||||||
style="pointer-events: none;"
|
style="pointer-events: none;"
|
||||||
/>
|
/>
|
||||||
@ -325,20 +322,18 @@
|
|||||||
|
|
||||||
<!-- 菜单类型 -->
|
<!-- 菜单类型 -->
|
||||||
<n-form-item label="菜单类型" path="menuType" class="mb-2">
|
<n-form-item label="菜单类型" path="menuType" class="mb-2">
|
||||||
<n-radio-group
|
<NRadioGroup
|
||||||
v-model:value="formData.menuType"
|
v-model:value="formData.menuType"
|
||||||
@update:value="handleMenuTypeChange"
|
@update:value="handleMenuTypeChange"
|
||||||
>
|
>
|
||||||
<n-radio value="1">
|
<NRadio
|
||||||
目录
|
v-for="item in dictOptions.sys_menu_type || []"
|
||||||
</n-radio>
|
:key="item.dictValue"
|
||||||
<n-radio value="2">
|
:value="item.dictValue"
|
||||||
菜单
|
>
|
||||||
</n-radio>
|
{{ item.dictLabel }}
|
||||||
<n-radio value="3">
|
</NRadio>
|
||||||
按钮
|
</NRadioGroup>
|
||||||
</n-radio>
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<!-- 菜单图标 -->
|
<!-- 菜单图标 -->
|
||||||
@ -392,14 +387,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">
|
||||||
<n-radio-group v-model:value="formData.isHide">
|
<NRadioGroup v-model:value="formData.isHide">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
@ -438,14 +433,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">
|
||||||
<n-radio-group v-model:value="formData.isKeepAlive">
|
<NRadioGroup v-model:value="formData.isKeepAlive">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
@ -462,26 +457,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">
|
||||||
<n-radio-group v-model:value="formData.isSpread">
|
<NRadioGroup v-model:value="formData.isSpread">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</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">
|
||||||
<n-radio-group v-model:value="formData.isAffix">
|
<NRadioGroup v-model:value="formData.isAffix">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@ -492,26 +487,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">
|
||||||
<n-radio-group v-model:value="formData.isSpread">
|
<NRadioGroup v-model:value="formData.isSpread">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</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">
|
||||||
<n-radio-group v-model:value="formData.isAffix">
|
<NRadioGroup v-model:value="formData.isAffix">
|
||||||
<n-radio value="0">
|
<NRadio value="0">
|
||||||
是
|
是
|
||||||
</n-radio>
|
</NRadio>
|
||||||
<n-radio value="1">
|
<NRadio value="1">
|
||||||
否
|
否
|
||||||
</n-radio>
|
</NRadio>
|
||||||
</n-radio-group>
|
</NRadioGroup>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@ -535,7 +530,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, NSpace, NSwitch, NTag } from 'naive-ui'
|
import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, 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'
|
||||||
@ -543,8 +538,9 @@ 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 { usePermission } from '@/hooks'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||||
import {
|
import {
|
||||||
addMenu,
|
addMenu,
|
||||||
@ -587,6 +583,11 @@ 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>()
|
||||||
@ -716,15 +717,7 @@ const columns: DataTableColumns<MenuVo> = [
|
|||||||
key: 'menuType',
|
key: 'menuType',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_menu_type', value: row.menuType }),
|
||||||
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: '展开/折叠',
|
||||||
@ -818,6 +811,8 @@ 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'),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -1315,7 +1310,8 @@ async function handleStatusChange(menu: MenuVo, status: string) {
|
|||||||
// 更新本地数据的状态
|
// 更新本地数据的状态
|
||||||
updateMenuStatusInData(tableData.value, menu.menuId, status)
|
updateMenuStatusInData(tableData.value, menu.menuId, status)
|
||||||
|
|
||||||
coiMsgSuccess('状态修改成功')
|
const statusLabel = getDictLabel('sys_switch_status', status, status === '0' ? '启用' : '停用')
|
||||||
|
coiMsgSuccess(`状态已更新为「${statusLabel}」`)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
coiMsgError('状态修改失败')
|
coiMsgError('状态修改失败')
|
||||||
|
|||||||
@ -40,10 +40,7 @@
|
|||||||
v-model:value="searchForm.operStatus"
|
v-model:value="searchForm.operStatus"
|
||||||
placeholder="请选择操作状态"
|
placeholder="请选择操作状态"
|
||||||
clearable
|
clearable
|
||||||
:options="[
|
:options="operStatusOptions"
|
||||||
{ label: '操作成功', value: '0' },
|
|
||||||
{ label: '操作失败', value: '1' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -233,9 +230,7 @@
|
|||||||
{{ currentLogDetail.costTime || '-' }}
|
{{ currentLogDetail.costTime || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="操作状态">
|
<n-descriptions-item label="操作状态">
|
||||||
<NTag :type="currentLogDetail.operStatus === '0' ? 'success' : 'error'" size="small">
|
<DictTag dict-type="sys_common_status" :value="currentLogDetail.operStatus === '0' ? '1' : '2'" />
|
||||||
{{ 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">
|
||||||
@ -255,7 +250,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, nextTick, onMounted, ref } from 'vue'
|
import { computed, 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'
|
||||||
@ -263,6 +258,7 @@ 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,
|
||||||
@ -273,10 +269,21 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
|
||||||
// 权限相关
|
// 权限相关
|
||||||
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)
|
||||||
@ -339,16 +346,9 @@ const columns: DataTableColumns<OperLogVo> = [
|
|||||||
{
|
{
|
||||||
title: '操作类型',
|
title: '操作类型',
|
||||||
key: 'operType',
|
key: 'operType',
|
||||||
width: 90,
|
width: 110,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_oper_type', value: row.operType }),
|
||||||
return h(NTag, {
|
|
||||||
type: 'primary',
|
|
||||||
size: 'small',
|
|
||||||
}, {
|
|
||||||
default: () => row.operType,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作人员[登录名/用户名]',
|
title: '操作人员[登录名/用户名]',
|
||||||
@ -419,15 +419,7 @@ const columns: DataTableColumns<OperLogVo> = [
|
|||||||
key: 'operStatus',
|
key: 'operStatus',
|
||||||
width: 90,
|
width: 90,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_common_status', value: row.operStatus === '0' ? '1' : '2' }),
|
||||||
const isSuccess = row.operStatus === '0'
|
|
||||||
return h(NTag, {
|
|
||||||
type: isSuccess ? 'success' : 'error',
|
|
||||||
size: 'small',
|
|
||||||
}, {
|
|
||||||
default: () => isSuccess ? '操作成功' : '操作失败',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作时间',
|
title: '操作时间',
|
||||||
|
|||||||
@ -72,7 +72,7 @@
|
|||||||
v-model:value="searchForm.pictureService"
|
v-model:value="searchForm.pictureService"
|
||||||
placeholder="请选择存储类型"
|
placeholder="请选择存储类型"
|
||||||
clearable
|
clearable
|
||||||
:options="PICTURE_SERVICE_OPTIONS"
|
:options="getSelectOptions('sys_file_service')"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -258,7 +258,9 @@
|
|||||||
<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><span class="text-gray-400">服务类型:</span> {{ getPictureServiceText(picture.pictureService) }}</div>
|
<div class="flex items-center gap-1">
|
||||||
|
<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>
|
||||||
@ -348,11 +350,7 @@
|
|||||||
<n-select
|
<n-select
|
||||||
v-model:value="uploadForm.pictureService"
|
v-model:value="uploadForm.pictureService"
|
||||||
placeholder="请选择服务类型"
|
placeholder="请选择服务类型"
|
||||||
:options="[
|
:options="getSelectOptions('sys_file_service')"
|
||||||
{ label: 'LOCAL', value: 'LOCAL' },
|
|
||||||
{ label: 'OSS', value: 'OSS' },
|
|
||||||
{ label: 'MINIO', value: 'MINIO' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
@ -417,13 +415,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 { usePermission } from '@/hooks/usePermission'
|
import DictTag from '@/components/common/DictTag.vue'
|
||||||
|
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'
|
||||||
@ -458,6 +456,35 @@ 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>()
|
||||||
@ -514,7 +541,7 @@ const isConfirmDisabled = computed(() => {
|
|||||||
|
|
||||||
// 上传表单数据
|
// 上传表单数据
|
||||||
const uploadForm = ref({
|
const uploadForm = ref({
|
||||||
pictureService: 'LOCAL',
|
pictureService: '1',
|
||||||
pictureType: '9', // 默认其他分类
|
pictureType: '9', // 默认其他分类
|
||||||
picturePath: '',
|
picturePath: '',
|
||||||
})
|
})
|
||||||
@ -622,20 +649,9 @@ const columns: DataTableColumns<SysPictureVo> = [
|
|||||||
{
|
{
|
||||||
title: '服务类型',
|
title: '服务类型',
|
||||||
key: 'pictureService',
|
key: 'pictureService',
|
||||||
width: 120,
|
width: 130,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_file_service', value: mapStorageTypeToValue(row.pictureService) }),
|
||||||
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: '创建时间',
|
||||||
@ -837,21 +853,12 @@ 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: 'LOCAL',
|
pictureService: '1',
|
||||||
pictureType: selectedCategory.value === '0' ? '9' : selectedCategory.value, // 根据当前分类设置默认值
|
pictureType: selectedCategory.value === '0' ? '9' : selectedCategory.value, // 根据当前分类设置默认值
|
||||||
picturePath: '',
|
picturePath: '',
|
||||||
}
|
}
|
||||||
@ -922,7 +929,8 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
onProgress({ percent: 10 })
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
// 调用上传API - 使用选择的分类和存储类型
|
// 调用上传API - 使用选择的分类和存储类型
|
||||||
const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
|
const storageType = mapServiceValueToType(uploadForm.value.pictureService)
|
||||||
|
const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, storageType)
|
||||||
|
|
||||||
// 检查业务逻辑是否成功
|
// 检查业务逻辑是否成功
|
||||||
if (result.isSuccess === false) {
|
if (result.isSuccess === false) {
|
||||||
|
|||||||
@ -61,10 +61,7 @@
|
|||||||
v-model:value="searchForm.roleStatus"
|
v-model:value="searchForm.roleStatus"
|
||||||
placeholder="请选择角色状态"
|
placeholder="请选择角色状态"
|
||||||
clearable
|
clearable
|
||||||
:options="[
|
:options="getSelectOptions('sys_switch_status')"
|
||||||
{ label: '启用', value: '0' },
|
|
||||||
{ label: '停用', value: '1' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -269,14 +266,15 @@
|
|||||||
<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">
|
||||||
<n-select
|
<NRadioGroup v-model:value="formData.roleStatus">
|
||||||
v-model:value="formData.roleStatus"
|
<NRadio
|
||||||
placeholder="请选择角色状态"
|
v-for="item in dictOptions.sys_switch_status || []"
|
||||||
:options="[
|
:key="item.dictValue"
|
||||||
{ label: '启用', value: '0' },
|
:value="item.dictValue"
|
||||||
{ 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>
|
||||||
@ -420,13 +418,14 @@
|
|||||||
<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, NSpace, NSwitch, NTag, NTree } from 'naive-ui'
|
import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, 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,
|
||||||
@ -446,10 +445,11 @@ 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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
|
||||||
// 权限相关
|
// 权限相关
|
||||||
const { hasButton } = usePermission()
|
const { hasButton } = usePermission()
|
||||||
|
const { dictOptions, getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -546,28 +546,27 @@ const columns: DataTableColumns<RoleVo> = [
|
|||||||
key: 'roleCode',
|
key: 'roleCode',
|
||||||
width: 150,
|
width: 150,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode }),
|
||||||
return h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '角色状态',
|
title: '角色状态',
|
||||||
key: 'roleStatus',
|
key: 'roleStatus',
|
||||||
width: 100,
|
width: 120,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return h('div', { class: 'flex items-center justify-center' }, [
|
return h('div', { class: 'flex items-center justify-center gap-2' }, [
|
||||||
|
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.roleStatus === '0' ? '停用' : '启用'}角色「${row.roleName}」吗?`,
|
default: () => `确定要将角色「${row.roleName}」状态切换为「${getDictLabel('sys_switch_status', row.roleStatus === '0' ? '1' : '0')}」吗?`,
|
||||||
trigger: () => h(NSwitch, {
|
trigger: () => h(NSwitch, {
|
||||||
value: row.roleStatus === '0',
|
value: row.roleStatus === '0',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
checkedChildren: '启用',
|
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
|
||||||
uncheckedChildren: '停用',
|
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@ -966,7 +965,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 = newStatus === '0' ? '启用' : '停用'
|
const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用')
|
||||||
|
|
||||||
const { isSuccess } = await updateRoleStatus(role.roleId, newStatus)
|
const { isSuccess } = await updateRoleStatus(role.roleId, newStatus)
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
|||||||
@ -72,10 +72,7 @@
|
|||||||
v-model:value="searchForm.userStatus"
|
v-model:value="searchForm.userStatus"
|
||||||
placeholder="请选择用户状态"
|
placeholder="请选择用户状态"
|
||||||
clearable
|
clearable
|
||||||
:options="[
|
:options="getSelectOptions('sys_switch_status')"
|
||||||
{ label: '启用', value: '0' },
|
|
||||||
{ label: '停用', value: '1' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -313,11 +310,7 @@
|
|||||||
<n-select
|
<n-select
|
||||||
v-model:value="formData.userType"
|
v-model:value="formData.userType"
|
||||||
placeholder="请选择用户类型"
|
placeholder="请选择用户类型"
|
||||||
:options="[
|
:options="getSelectOptions('sys_user_type')"
|
||||||
{ label: '系统用户', value: '1' },
|
|
||||||
{ label: '注册用户', value: '2' },
|
|
||||||
{ label: '微信用户', value: '3' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -327,11 +320,7 @@
|
|||||||
<n-select
|
<n-select
|
||||||
v-model:value="formData.sex"
|
v-model:value="formData.sex"
|
||||||
placeholder="请选择性别"
|
placeholder="请选择性别"
|
||||||
:options="[
|
:options="getSelectOptions('sys_user_sex')"
|
||||||
{ label: '男', value: '1' },
|
|
||||||
{ label: '女', value: '2' },
|
|
||||||
{ label: '未知', value: '3' },
|
|
||||||
]"
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@ -367,14 +356,15 @@
|
|||||||
|
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="用户状态" path="userStatus" class="mb-2">
|
<n-form-item label="用户状态" path="userStatus" class="mb-2">
|
||||||
<n-radio-group v-model:value="formData.userStatus">
|
<NRadioGroup v-model:value="formData.userStatus">
|
||||||
<n-radio value="0">
|
<NRadio
|
||||||
启用
|
v-for="item in dictOptions.sys_switch_status || []"
|
||||||
</n-radio>
|
:key="item.dictValue"
|
||||||
<n-radio value="1">
|
:value="item.dictValue"
|
||||||
停用
|
>
|
||||||
</n-radio>
|
{{ item.dictLabel }}
|
||||||
</n-radio-group>
|
</NRadio>
|
||||||
|
</NRadioGroup>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
|
|
||||||
@ -585,11 +575,8 @@
|
|||||||
{{ 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>用户类型: {{
|
<span>用户类型: {{ getDictLabel('sys_user_type', currentAvatarUser?.userType, '--') }}</span>
|
||||||
currentAvatarUser?.userType === '1' ? '系统用户'
|
<span>状态: {{ getDictLabel('sys_switch_status', currentAvatarUser?.userStatus, '--') }}</span>
|
||||||
: currentAvatarUser?.userType === '2' ? '注册用户' : '微信用户'
|
|
||||||
}}</span>
|
|
||||||
<span>状态: {{ currentAvatarUser?.userStatus === '0' ? '启用' : '停用' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -704,7 +691,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, NSpace, NSwitch, NTag, NUpload, NUploadDragger } from 'naive-ui'
|
import { NButton, NCheckbox, NDropdown, NIcon, NPopconfirm, NProgress, NRadio, NRadioGroup, 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'
|
||||||
@ -715,6 +702,7 @@ 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,
|
||||||
@ -739,7 +727,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 { usePermission } from '@/hooks/usePermission'
|
import { useDict, usePermission } from '@/hooks'
|
||||||
|
|
||||||
// 权限相关
|
// 权限相关
|
||||||
const { hasButton } = usePermission()
|
const { hasButton } = usePermission()
|
||||||
@ -790,6 +778,12 @@ 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,
|
||||||
@ -956,33 +950,14 @@ const columns: DataTableColumns<UserVo> = [
|
|||||||
key: 'userType',
|
key: 'userType',
|
||||||
width: 100,
|
width: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (row) => {
|
render: row => h(DictTag, { dictType: 'sys_user_type', value: row.userType }),
|
||||||
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) => {
|
render: row => h(DictTag, { dictType: 'sys_user_sex', value: row.sex }),
|
||||||
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: '用户状态',
|
||||||
@ -990,18 +965,21 @@ 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.userStatus === '0' ? '停用' : '启用'}用户「${row.userName}」吗?`,
|
default: () => `确定要将用户「${row.userName}」状态切换为「${getDictLabel('sys_switch_status', nextStatus)}」吗?`,
|
||||||
trigger: () => h(NSwitch, {
|
trigger: () => h(NSwitch, {
|
||||||
value: row.userStatus === '0',
|
value: row.userStatus === '0',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
checkedChildren: '启用',
|
checkedChildren: enableLabel,
|
||||||
uncheckedChildren: '停用',
|
uncheckedChildren: disableLabel,
|
||||||
loading: false,
|
loading: false,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@ -1357,7 +1335,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 = newStatus === '0' ? '启用' : '停用'
|
const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用')
|
||||||
|
|
||||||
const { isSuccess } = await updateUserStatus(user.userId, newStatus)
|
const { isSuccess } = await updateUserStatus(user.userId, newStatus)
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user