yumi-admin/apps/src/views/app-system/components/enum-setting-config-panel.vue
2026-03-31 17:40:10 +08:00

422 lines
9.0 KiB
Vue

<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useSortable } from '@vben/hooks';
import type { LegacyEnumConfigItem } from '#/api/legacy/app-system';
import {
getEnumConfigByGroup,
updateConfigInfo,
updateEnumConfigSort,
} from '#/api/legacy/app-system';
import { ENUM_CONFIG_DATA_TYPE_EXPRESSION } from '#/views/system/shared';
import {
Button,
Empty,
Input,
Modal,
Space,
Spin,
Switch,
Tooltip,
message,
} from 'antdv-next';
const props = defineProps<{
group: string;
}>();
const loading = ref(false);
const sortLoading = ref(false);
const list = ref<LegacyEnumConfigItem[]>([]);
const dirty = ref(false);
const editOpen = ref(false);
const editLoading = ref(false);
const editValue = ref('');
const containerRef = ref<HTMLElement>();
const activeItem = ref<LegacyEnumConfigItem | null>(null);
let sortableInstance: any = null;
function isBooleanType(item?: LegacyEnumConfigItem) {
return item?.dataType === 'bool';
}
function getDisplayValue(item?: LegacyEnumConfigItem) {
if (!item) {
return '-';
}
if (isBooleanType(item)) {
if (item.val === 'true') {
return '开';
}
if (item.val === 'false') {
return '关';
}
}
return item.val || '-';
}
async function copyText(text?: string) {
const value = String(text || '');
if (!value) {
message.warning('暂无可复制内容');
return;
}
try {
await navigator.clipboard.writeText(value);
message.success('复制成功');
} catch {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.setAttribute('readonly', 'readonly');
textarea.style.left = '-9999px';
textarea.style.position = 'fixed';
document.body.append(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
message.success('复制成功');
}
}
async function initSortable() {
if (!containerRef.value || sortableInstance) {
return;
}
const { initializeSortable } = useSortable(containerRef.value, {
animation: 150,
ghostClass: 'enum-setting-item--dragging',
handle: '.drag-handle',
onEnd(event) {
const oldIndex = event.oldIndex ?? -1;
const newIndex = event.newIndex ?? -1;
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {
return;
}
const next = [...list.value];
next.splice(newIndex, 0, next.splice(oldIndex, 1)[0]!);
list.value = next;
dirty.value = true;
},
});
sortableInstance = await initializeSortable();
}
async function loadData() {
if (!props.group) {
list.value = [];
return;
}
loading.value = true;
try {
const result = await getEnumConfigByGroup(props.group);
list.value = result.result || [];
dirty.value = false;
await nextTick();
await initSortable();
} finally {
loading.value = false;
}
}
function openEdit(item: LegacyEnumConfigItem) {
const expression = ENUM_CONFIG_DATA_TYPE_EXPRESSION[item.dataType || ''];
if (!expression) {
message.error('未找到相关定义数据类型,请联系管理员');
return;
}
activeItem.value = item;
editValue.value = String(item.val || '');
editOpen.value = true;
}
async function handleUpdateValue() {
const item = activeItem.value;
if (!item) {
return;
}
const expression = ENUM_CONFIG_DATA_TYPE_EXPRESSION[item.dataType || ''];
if (!expression) {
message.error('未找到相关定义数据类型,请联系管理员');
return;
}
if (expression.rex && !expression.rex.test(editValue.value)) {
message.warning(`数据类型错误: ${expression.msg}`);
return;
}
editLoading.value = true;
try {
await updateConfigInfo({
id: item.id,
name: item.name,
val: editValue.value,
});
message.success('保存成功');
editOpen.value = false;
await loadData();
} finally {
editLoading.value = false;
}
}
async function handleSwitchChange(checked: boolean, item: LegacyEnumConfigItem) {
const previous = item.val;
item.val = checked ? 'true' : 'false';
try {
await updateConfigInfo({
id: item.id,
name: item.name,
val: item.val,
});
message.success('保存成功');
} catch (error) {
item.val = previous;
throw error;
}
}
async function handleSortSave() {
if (list.value.length === 0) {
return;
}
const size = list.value.length;
const payload = list.value.map((item, index) => ({
id: item.id,
sort: size - index,
}));
sortLoading.value = true;
try {
await updateEnumConfigSort(payload);
message.success('排序已保存');
dirty.value = false;
await loadData();
} finally {
sortLoading.value = false;
}
}
watch(
() => props.group,
() => {
loadData();
},
{ immediate: true },
);
watch(
() => list.value.length,
() => {
nextTick(initSortable);
},
);
onMounted(() => {
nextTick(initSortable);
});
onUnmounted(() => {
sortableInstance?.destroy?.();
sortableInstance = null;
});
</script>
<template>
<div class="panel-root">
<div class="panel-tools">
<Button
v-if="dirty"
:loading="sortLoading"
danger
type="primary"
@click="handleSortSave"
>
保存排序
</Button>
</div>
<Spin :spinning="loading">
<div v-if="list.length > 0" ref="containerRef" class="setting-list">
<div
v-for="item in list"
:key="item.id"
class="setting-item"
>
<div class="drag-handle" title="拖拽排序">
</div>
<div class="setting-copy">
<div class="setting-title-row">
<Tooltip v-if="item.description">
<template #title>
<div class="tooltip-pre">{{ item.description }}</div>
</template>
<span class="setting-title">{{ item.title }}</span>
</Tooltip>
<span v-else class="setting-title">{{ item.title }}</span>
<Button size="small" type="link" @click="copyText(item.name)">
复制键
</Button>
</div>
<div class="setting-desc">
{{ item.description || item.name || '-' }}
</div>
</div>
<div class="setting-value">
{{ getDisplayValue(item) }}
</div>
<div class="setting-action">
<Switch
v-if="isBooleanType(item)"
:checked="item.val === 'true'"
:disabled="item.inoperable"
checked-children=""
un-checked-children=""
@change="(checked: boolean) => handleSwitchChange(checked, item)"
/>
<Button
v-else
:disabled="item.inoperable"
type="link"
@click="openEdit(item)"
>
编辑
</Button>
</div>
</div>
</div>
<Empty v-else description="暂无数据" />
</Spin>
<Modal
:confirm-loading="editLoading"
:open="editOpen"
destroy-on-close
ok-text="保存"
title="编辑参数"
@cancel="editOpen = false"
@ok="handleUpdateValue"
>
<Space direction="vertical" style="width: 100%">
<div class="edit-tip">
{{
ENUM_CONFIG_DATA_TYPE_EXPRESSION[activeItem?.dataType || '']?.msg ||
'请输入参数值'
}}
</div>
<Input
v-model:value="editValue"
placeholder="请输入参数值"
/>
</Space>
</Modal>
</div>
</template>
<style scoped>
.panel-root {
margin-top: 16px;
}
.panel-tools {
margin-bottom: 16px;
}
.setting-list {
display: grid;
gap: 12px;
}
.setting-item {
align-items: center;
background: linear-gradient(180deg, #fff, #f8fafc);
border: 1px solid #e2e8f0;
border-radius: 18px;
display: grid;
gap: 16px;
grid-template-columns: 32px minmax(220px, 1.8fr) minmax(160px, 1fr) 140px;
padding: 18px 20px;
}
.drag-handle {
color: #94a3b8;
cursor: move;
font-size: 18px;
line-height: 1;
text-align: center;
user-select: none;
}
.setting-copy {
min-width: 0;
}
.setting-title-row {
align-items: center;
display: flex;
gap: 8px;
margin-bottom: 6px;
}
.setting-title {
color: #0f172a;
font-size: 15px;
font-weight: 600;
}
.setting-desc {
color: #64748b;
font-size: 13px;
line-height: 1.6;
word-break: break-word;
}
.setting-value {
color: #1e293b;
font-size: 14px;
line-height: 1.7;
word-break: break-word;
}
.setting-action {
align-items: center;
display: flex;
justify-content: flex-end;
}
.tooltip-pre {
max-width: 360px;
white-space: pre-wrap;
word-break: break-word;
}
.edit-tip {
color: #64748b;
font-size: 13px;
}
:deep(.enum-setting-item--dragging) {
background: #eff6ff;
border-color: #93c5fd;
}
@media (max-width: 960px) {
.setting-item {
grid-template-columns: 24px 1fr;
}
.setting-value,
.setting-action {
grid-column: 2;
justify-content: flex-start;
}
}
</style>