格式化代码,websocket功能完善
This commit is contained in:
@@ -1,25 +1,56 @@
|
||||
<template>
|
||||
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="700px">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-modal
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:confirm-loading="isSaving"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
width="700px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 配置名称 -->
|
||||
<a-form-item label="配置名称" name="name" required>
|
||||
<a-input v-model:value="form.name" placeholder="如:网站名称" allow-clear />
|
||||
<a-input
|
||||
v-model:value="form.name"
|
||||
placeholder="如:网站名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 配置键名 -->
|
||||
<a-form-item label="配置键名" name="key" required>
|
||||
<a-input v-model:value="form.key" placeholder="如:site_name" allow-clear :disabled="isEdit" />
|
||||
<div class="form-tip">系统唯一标识,只能包含字母、数字、下划线</div>
|
||||
<a-input
|
||||
v-model:value="form.key"
|
||||
placeholder="如:site_name"
|
||||
allow-clear
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
系统唯一标识,只能包含字母、数字、下划线
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 配置分组 -->
|
||||
<a-form-item label="配置分组" name="group" required>
|
||||
<a-select v-model:value="form.group" placeholder="请选择配置分组" :options="groupOptions" />
|
||||
<a-select
|
||||
v-model:value="form.group"
|
||||
placeholder="请选择配置分组"
|
||||
:options="groupOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 配置类型 -->
|
||||
<a-form-item label="配置类型" name="type" required>
|
||||
<a-select v-model:value="form.type" placeholder="请选择配置类型">
|
||||
<a-select
|
||||
v-model:value="form.type"
|
||||
placeholder="请选择配置类型"
|
||||
>
|
||||
<a-select-option value="string">字符串</a-select-option>
|
||||
<a-select-option value="number">数字</a-select-option>
|
||||
<a-select-option value="boolean">布尔值</a-select-option>
|
||||
@@ -34,41 +65,88 @@
|
||||
<a-form-item label="配置值" name="value">
|
||||
<!-- 字符串/文本 -->
|
||||
<template v-if="['string', 'text'].includes(form.type)">
|
||||
<a-input v-if="form.type === 'string'" v-model:value="form.value" placeholder="请输入配置值" allow-clear />
|
||||
<a-textarea v-else v-model:value="form.value" placeholder="请输入配置值" :rows="4" />
|
||||
<a-input
|
||||
v-if="form.type === 'string'"
|
||||
v-model:value="form.value"
|
||||
placeholder="请输入配置值"
|
||||
allow-clear
|
||||
/>
|
||||
<a-textarea
|
||||
v-else
|
||||
v-model:value="form.value"
|
||||
placeholder="请输入配置值"
|
||||
:rows="4"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 数字 -->
|
||||
<a-input-number v-else-if="form.type === 'number'" v-model:value="form.value" :min="0" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-else-if="form.type === 'number'"
|
||||
v-model:value="form.value"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
|
||||
<!-- 布尔值 -->
|
||||
<a-switch v-else-if="form.type === 'boolean'" v-model:checked="valueChecked" checked-children="启用" un-checked-children="禁用" />
|
||||
<a-switch
|
||||
v-else-if="form.type === 'boolean'"
|
||||
v-model:checked="valueChecked"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
|
||||
<!-- 文件/图片 -->
|
||||
<a-input v-else-if="['file', 'image'].includes(form.type)" v-model:value="form.value" placeholder="请输入文件地址" />
|
||||
<a-input
|
||||
v-else-if="['file', 'image'].includes(form.type)"
|
||||
v-model:value="form.value"
|
||||
placeholder="请输入文件地址"
|
||||
/>
|
||||
|
||||
<!-- 选择框 -->
|
||||
<a-input v-else v-model:value="form.optionsText" placeholder="选项值,用逗号分隔,如:选项1,选项2,选项3" />
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="form.optionsText"
|
||||
placeholder="选项值,用逗号分隔,如:选项1,选项2,选项3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 默认值 -->
|
||||
<a-form-item label="默认值" name="default_value">
|
||||
<a-input v-model:value="form.default_value" placeholder="默认值(可选)" allow-clear />
|
||||
<a-input
|
||||
v-model:value="form.default_value"
|
||||
placeholder="默认值(可选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 排序 -->
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="form.sort"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
|
||||
<a-switch
|
||||
v-model:checked="statusChecked"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 描述 -->
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="form.description" placeholder="请输入配置描述" :rows="3" maxlength="200" show-count />
|
||||
<a-textarea
|
||||
v-model:value="form.description"
|
||||
placeholder="请输入配置描述"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -76,215 +154,228 @@
|
||||
<div class="dialog-footer">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleSubmit"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { useDictionaryStore } from '@/stores/modules/dictionary'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import systemApi from "@/api/system";
|
||||
import { useDictionaryStore } from "@/stores/modules/dictionary";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref(null)
|
||||
const isSaving = ref(false)
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
const formRef = ref(null);
|
||||
const isSaving = ref(false);
|
||||
const isEdit = computed(() => !!props.record?.id);
|
||||
|
||||
const title = computed(() => {
|
||||
return isEdit.value ? '编辑配置' : '新增配置'
|
||||
})
|
||||
return isEdit.value ? "编辑配置" : "新增配置";
|
||||
});
|
||||
|
||||
// 配置分组选项
|
||||
const groupOptions = ref([])
|
||||
const groupOptions = ref([]);
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
key: '',
|
||||
group: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
default_value: '',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
key: "",
|
||||
group: "",
|
||||
type: "string",
|
||||
value: "",
|
||||
default_value: "",
|
||||
description: "",
|
||||
status: true,
|
||||
sort: 0,
|
||||
options: []
|
||||
})
|
||||
options: [],
|
||||
});
|
||||
|
||||
// 选项文本(用于选择框类型)
|
||||
const optionsText = ref('')
|
||||
const optionsText = ref("");
|
||||
|
||||
// 计算属性:状态开关
|
||||
const statusChecked = computed({
|
||||
get: () => form.value.status === true,
|
||||
set: (val) => {
|
||||
form.value.status = val ? true : false
|
||||
}
|
||||
})
|
||||
form.value.status = val ? true : false;
|
||||
},
|
||||
});
|
||||
|
||||
// 计算属性:值开关(布尔类型)
|
||||
const valueChecked = computed({
|
||||
get: () => form.value.value === '1' || form.value.value === true,
|
||||
get: () => form.value.value === "1" || form.value.value === true,
|
||||
set: (val) => {
|
||||
form.value.value = val ? '1' : '0'
|
||||
}
|
||||
})
|
||||
form.value.value = val ? "1" : "0";
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化字典 store
|
||||
const dictionaryStore = useDictionaryStore()
|
||||
const dictionaryStore = useDictionaryStore();
|
||||
|
||||
// 加载配置分组
|
||||
const loadGroups = async () => {
|
||||
const groups = await dictionaryStore.getDictionary('config_group')
|
||||
groupOptions.value = groups.map(item => ({
|
||||
const groups = await dictionaryStore.getDictionary("config_group");
|
||||
groupOptions.value = groups.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
}))
|
||||
}
|
||||
value: item.value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 验证规则
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
{ required: true, message: "请输入配置名称", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
key: [
|
||||
{ required: true, message: '请输入配置键名', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/, message: '格式不正确', trigger: 'blur' }
|
||||
{ required: true, message: "请输入配置键名", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: "格式不正确",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
group: [
|
||||
{ required: true, message: '请选择配置分组', trigger: 'change' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
group: [{ required: true, message: "请选择配置分组", trigger: "change" }],
|
||||
type: [{ required: true, message: "请选择配置类型", trigger: "change" }],
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
key: '',
|
||||
group: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
default_value: '',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
key: "",
|
||||
group: "",
|
||||
type: "string",
|
||||
value: "",
|
||||
default_value: "",
|
||||
description: "",
|
||||
status: true,
|
||||
sort: 0,
|
||||
options: []
|
||||
}
|
||||
optionsText.value = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
options: [],
|
||||
};
|
||||
optionsText.value = "";
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 设置数据
|
||||
const setData = (data) => {
|
||||
if (data) {
|
||||
form.value = {
|
||||
id: data.id || '',
|
||||
name: data.name || '',
|
||||
key: data.key || '',
|
||||
group: data.group || '',
|
||||
type: data.type || 'string',
|
||||
value: data.value || '',
|
||||
default_value: data.default_value || '',
|
||||
description: data.description || '',
|
||||
id: data.id || "",
|
||||
name: data.name || "",
|
||||
key: data.key || "",
|
||||
group: data.group || "",
|
||||
type: data.type || "string",
|
||||
value: data.value || "",
|
||||
default_value: data.default_value || "",
|
||||
description: data.description || "",
|
||||
status: data.status !== undefined ? data.status : true,
|
||||
sort: data.sort !== undefined ? data.sort : 0,
|
||||
options: data.options || []
|
||||
}
|
||||
options: data.options || [],
|
||||
};
|
||||
|
||||
// 如果是选择框类型,设置选项文本
|
||||
if (data.type === 'select' && data.options) {
|
||||
optionsText.value = Array.isArray(data.options) ? data.options.join(',') : data.options
|
||||
if (data.type === "select" && data.options) {
|
||||
optionsText.value = Array.isArray(data.options)
|
||||
? data.options.join(",")
|
||||
: data.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
await formRef.value.validate();
|
||||
|
||||
isSaving.value = true
|
||||
isSaving.value = true;
|
||||
|
||||
// 处理选项
|
||||
if (form.value.type === 'select') {
|
||||
form.value.options = optionsText.value ? optionsText.value.split(',').map(s => s.trim()) : []
|
||||
if (form.value.type === "select") {
|
||||
form.value.options = optionsText.value
|
||||
? optionsText.value.split(",").map((s) => s.trim())
|
||||
: [];
|
||||
}
|
||||
|
||||
// 处理值
|
||||
let submitValue = form.value.value
|
||||
if (form.value.type === 'boolean') {
|
||||
submitValue = valueChecked.value ? '1' : '0'
|
||||
let submitValue = form.value.value;
|
||||
if (form.value.type === "boolean") {
|
||||
submitValue = valueChecked.value ? "1" : "0";
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...form.value,
|
||||
value: submitValue
|
||||
}
|
||||
value: submitValue,
|
||||
};
|
||||
|
||||
let res = {}
|
||||
let res = {};
|
||||
if (isEdit.value) {
|
||||
res = await systemApi.config.update.put(form.value.id, submitData)
|
||||
res = await systemApi.config.update.put(form.value.id, submitData);
|
||||
} else {
|
||||
res = await systemApi.config.add.post(submitData)
|
||||
res = await systemApi.config.add.post(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
message.success(isEdit.value ? "编辑成功" : "新增成功");
|
||||
emit("success");
|
||||
handleCancel();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
console.log('表单验证失败:', error)
|
||||
console.log("表单验证失败:", error);
|
||||
} else {
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败')
|
||||
console.error("提交失败:", error);
|
||||
message.error("操作失败");
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
resetForm();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadGroups()
|
||||
if (props.record) {
|
||||
setData(props.record)
|
||||
} else {
|
||||
resetForm()
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
loadGroups();
|
||||
if (props.record) {
|
||||
setData(props.record);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,59 +1,141 @@
|
||||
<template>
|
||||
<div class="config-form-container">
|
||||
<a-form ref="formRef" :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<!-- 根据配置类型动态渲染表单项 -->
|
||||
<template v-for="config in configs" :key="config.id">
|
||||
<!-- 字符串类型 -->
|
||||
<a-form-item v-if="config.type === 'string'" :label="config.name" :name="config.key">
|
||||
<a-input v-model:value="formData[config.key]" :placeholder="`请输入${config.name}`" />
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<a-form-item
|
||||
v-if="config.type === 'string'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData[config.key]"
|
||||
:placeholder="`请输入${config.name}`"
|
||||
/>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 数字类型 -->
|
||||
<a-form-item v-else-if="config.type === 'number'" :label="config.name" :name="config.key">
|
||||
<a-input-number v-model:value="formData[config.key]" :min="config.options?.min || 0"
|
||||
:max="config.options?.max || 100000" style="width: 100%" />
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'number'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:value="formData[config.key]"
|
||||
:min="config.options?.min || 0"
|
||||
:max="config.options?.max || 100000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 布尔类型 -->
|
||||
<a-form-item v-else-if="config.type === 'boolean'" :label="config.name" :name="config.key">
|
||||
<a-switch v-model:checked="formData[config.key]" checked-children="启用" un-checked-children="禁用" />
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'boolean'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-switch
|
||||
v-model:checked="formData[config.key]"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 文件类型 -->
|
||||
<a-form-item v-else-if="config.type === 'file'" :label="config.name" :name="config.key">
|
||||
<a-input v-model:value="formData[config.key]" :placeholder="`请输入${config.name}地址`">
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'file'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="formData[config.key]"
|
||||
:placeholder="`请输入${config.name}地址`"
|
||||
>
|
||||
<template #suffix>
|
||||
<a-button type="link" size="small" @click="handleUpload(config)">上传</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleUpload(config)"
|
||||
>上传</a-button
|
||||
>
|
||||
</template>
|
||||
</a-input>
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 图片类型 -->
|
||||
<a-form-item v-else-if="config.type === 'image'" :label="config.name" :name="config.key">
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'image'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<div class="image-upload-wrapper">
|
||||
<a-input v-model:value="formData[config.key]" :placeholder="`请输入${config.name}地址`" />
|
||||
<a-input
|
||||
v-model:value="formData[config.key]"
|
||||
:placeholder="`请输入${config.name}地址`"
|
||||
/>
|
||||
<div v-if="formData[config.key]" class="image-preview">
|
||||
<img :src="formData[config.key]" :alt="config.name" />
|
||||
<img
|
||||
:src="formData[config.key]"
|
||||
:alt="config.name"
|
||||
/>
|
||||
</div>
|
||||
<a-button type="link" @click="handleUpload(config)">选择图片</a-button>
|
||||
<a-button type="link" @click="handleUpload(config)"
|
||||
>选择图片</a-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 文本类型 -->
|
||||
<a-form-item v-else-if="config.type === 'text'" :label="config.name" :name="config.key">
|
||||
<a-textarea v-model:value="formData[config.key]" :rows="4" :placeholder="`请输入${config.name}`" />
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'text'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="formData[config.key]"
|
||||
:rows="4"
|
||||
:placeholder="`请输入${config.name}`"
|
||||
/>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 选择框类型 -->
|
||||
<a-form-item v-else-if="config.type === 'select'" :label="config.name" :name="config.key">
|
||||
<a-select v-model:value="formData[config.key]" :placeholder="`请选择${config.name}`" :options="config.options || []" />
|
||||
<div v-if="config.description" class="form-tip">{{ config.description }}</div>
|
||||
<a-form-item
|
||||
v-else-if="config.type === 'select'"
|
||||
:label="config.name"
|
||||
:name="config.key"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="formData[config.key]"
|
||||
:placeholder="`请选择${config.name}`"
|
||||
:options="config.options || []"
|
||||
/>
|
||||
<div v-if="config.description" class="form-tip">
|
||||
{{ config.description }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
@@ -74,102 +156,105 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { SaveOutlined, RedoOutlined } from '@ant-design/icons-vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { SaveOutlined, RedoOutlined } from "@ant-design/icons-vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
const emit = defineEmits(["refresh"]);
|
||||
|
||||
const formRef = ref(null)
|
||||
const saving = ref(false)
|
||||
const configs = ref([])
|
||||
const formData = reactive({})
|
||||
const formRef = ref(null);
|
||||
const saving = ref(false);
|
||||
const configs = ref([]);
|
||||
const formData = reactive({});
|
||||
|
||||
// 加载配置
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
const res = await systemApi.config.list.get({ group: props.group })
|
||||
const res = await systemApi.config.list.get({ group: props.group });
|
||||
if (res.code === 200) {
|
||||
configs.value = res.data.list || []
|
||||
configs.value = res.data.list || [];
|
||||
|
||||
// 初始化表单数据
|
||||
configs.value.forEach(config => {
|
||||
configs.value.forEach((config) => {
|
||||
// 根据类型转换值
|
||||
if (config.type === 'boolean') {
|
||||
formData[config.key] = config.value === '1' || config.value === true
|
||||
} else if (config.type === 'number') {
|
||||
formData[config.key] = Number(config.value) || 0
|
||||
if (config.type === "boolean") {
|
||||
formData[config.key] =
|
||||
config.value === "1" || config.value === true;
|
||||
} else if (config.type === "number") {
|
||||
formData[config.key] = Number(config.value) || 0;
|
||||
} else {
|
||||
formData[config.key] = config.value || ''
|
||||
formData[config.key] = config.value || "";
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
message.error('加载配置失败')
|
||||
console.error("加载配置失败:", error);
|
||||
message.error("加载配置失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
saving.value = true;
|
||||
|
||||
// 准备提交数据
|
||||
const updates = []
|
||||
configs.value.forEach(config => {
|
||||
let value = formData[config.key]
|
||||
const updates = [];
|
||||
configs.value.forEach((config) => {
|
||||
let value = formData[config.key];
|
||||
|
||||
// 根据类型转换值
|
||||
if (config.type === 'boolean') {
|
||||
value = value ? '1' : '0'
|
||||
} else if (config.type === 'number') {
|
||||
value = String(value)
|
||||
if (config.type === "boolean") {
|
||||
value = value ? "1" : "0";
|
||||
} else if (config.type === "number") {
|
||||
value = String(value);
|
||||
}
|
||||
|
||||
updates.push({
|
||||
key: config.key,
|
||||
value: value
|
||||
})
|
||||
})
|
||||
value: value,
|
||||
});
|
||||
});
|
||||
|
||||
const res = await systemApi.config.batchUpdate.post({ configs: updates })
|
||||
const res = await systemApi.config.batchUpdate.post({
|
||||
configs: updates,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('保存成功')
|
||||
emit('refresh')
|
||||
message.success("保存成功");
|
||||
emit("refresh");
|
||||
} else {
|
||||
message.error(res.message || '保存失败')
|
||||
message.error(res.message || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
message.error('保存失败')
|
||||
console.error("保存配置失败:", error);
|
||||
message.error("保存失败");
|
||||
} finally {
|
||||
saving.value = false
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const handleReset = () => {
|
||||
loadConfigs()
|
||||
message.info('已重置')
|
||||
}
|
||||
loadConfigs();
|
||||
message.info("已重置");
|
||||
};
|
||||
|
||||
// 上传处理(占位)
|
||||
const handleUpload = (config) => {
|
||||
message.info(`${config.name}上传功能待实现`)
|
||||
}
|
||||
message.info(`${config.name}上传功能待实现`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadConfigs()
|
||||
})
|
||||
loadConfigs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="配置分组" name="group">
|
||||
<a-select v-model:value="formData.group" placeholder="请选择配置分组" allow-show-search>
|
||||
<a-select
|
||||
v-model:value="formData.group"
|
||||
placeholder="请选择配置分组"
|
||||
allow-show-search
|
||||
>
|
||||
<a-select-option value="system">系统设置</a-select-option>
|
||||
<a-select-option value="site">站点配置</a-select-option>
|
||||
<a-select-option value="upload">上传配置</a-select-option>
|
||||
@@ -18,24 +28,45 @@
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
<template #notFoundContent>
|
||||
<div style="text-align: center">
|
||||
<a-input v-model:value="customGroup" placeholder="输入新分组" style="margin-bottom: 8px" />
|
||||
<a-button type="primary" size="small" @click="handleAddCustomGroup">添加</a-button>
|
||||
<a-input
|
||||
v-model:value="customGroup"
|
||||
placeholder="输入新分组"
|
||||
style="margin-bottom: 8px"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAddCustomGroup"
|
||||
>添加</a-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置键" name="key">
|
||||
<a-input v-model:value="formData.key" placeholder="请输入配置键,如:site_name" :disabled="isEdit && formData.is_system" />
|
||||
<div style="color: #999; font-size: 12px; margin-top: 4px">唯一标识,建议使用英文下划线命名</div>
|
||||
<a-input
|
||||
v-model:value="formData.key"
|
||||
placeholder="请输入配置键,如:site_name"
|
||||
:disabled="isEdit && formData.is_system"
|
||||
/>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 4px">
|
||||
唯一标识,建议使用英文下划线命名
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入配置名称" />
|
||||
<a-input
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入配置名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="数据类型" name="type">
|
||||
<a-select v-model:value="formData.type" placeholder="请选择数据类型">
|
||||
<a-select
|
||||
v-model:value="formData.type"
|
||||
placeholder="请选择数据类型"
|
||||
>
|
||||
<a-select-option value="string">字符串</a-select-option>
|
||||
<a-select-option value="text">文本</a-select-option>
|
||||
<a-select-option value="number">数字</a-select-option>
|
||||
@@ -99,7 +130,11 @@
|
||||
placeholder="请选择配置值"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option v-for="opt in parsedOptions" :key="opt.value" :value="opt.value">
|
||||
<a-select-option
|
||||
v-for="opt in parsedOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -108,7 +143,11 @@
|
||||
v-else-if="formData.type === 'checkbox'"
|
||||
v-model:value="formData.value"
|
||||
>
|
||||
<a-checkbox v-for="opt in parsedOptions" :key="opt.value" :value="opt.value">
|
||||
<a-checkbox
|
||||
v-for="opt in parsedOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
@@ -132,7 +171,12 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="formData.sort" placeholder="请输入排序" :min="0" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
@@ -143,113 +187,117 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入配置描述" :rows="2" />
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入配置描述"
|
||||
:rows="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, reactive, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const customGroup = ref('')
|
||||
const optionsText = ref('')
|
||||
const formRef = ref(null);
|
||||
const loading = ref(false);
|
||||
const customGroup = ref("");
|
||||
const optionsText = ref("");
|
||||
|
||||
// 是否编辑模式
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
const isEdit = computed(() => !!props.record?.id);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
group: 'system',
|
||||
key: '',
|
||||
name: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
group: "system",
|
||||
key: "",
|
||||
name: "",
|
||||
type: "string",
|
||||
value: "",
|
||||
options: null,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
description: '',
|
||||
is_system: false
|
||||
})
|
||||
description: "",
|
||||
is_system: false,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
group: [{ required: true, message: '请选择配置分组', trigger: 'change' }],
|
||||
key: [{ required: true, message: '请输入配置键', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
|
||||
value: [{ required: true, message: '请输入配置值', trigger: 'change' }]
|
||||
}
|
||||
group: [{ required: true, message: "请选择配置分组", trigger: "change" }],
|
||||
key: [{ required: true, message: "请输入配置键", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入配置名称", trigger: "blur" }],
|
||||
type: [{ required: true, message: "请选择数据类型", trigger: "change" }],
|
||||
value: [{ required: true, message: "请输入配置值", trigger: "change" }],
|
||||
};
|
||||
|
||||
// 解析选项
|
||||
const parsedOptions = computed(() => {
|
||||
if (!optionsText.value) return []
|
||||
if (!optionsText.value) return [];
|
||||
try {
|
||||
return optionsText.value
|
||||
.split('\n')
|
||||
.split("\n")
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
const [label, value] = line.split(':').map((s) => s.trim())
|
||||
return { label, value: value || label }
|
||||
})
|
||||
const [label, value] = line.split(":").map((s) => s.trim());
|
||||
return { label, value: value || label };
|
||||
});
|
||||
} catch {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 根据类型解析值
|
||||
const parseValueByType = (value, type) => {
|
||||
if (!value) return value
|
||||
if (!value) return value;
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return Number(value)
|
||||
case 'boolean':
|
||||
return value === 'true' || value === true || value === 1
|
||||
case 'checkbox':
|
||||
if (typeof value === 'string') {
|
||||
case "number":
|
||||
return Number(value);
|
||||
case "boolean":
|
||||
return value === "true" || value === true || value === 1;
|
||||
case "checkbox":
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value.split(',')
|
||||
return value.split(",");
|
||||
}
|
||||
}
|
||||
return value
|
||||
return value;
|
||||
default:
|
||||
return value
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
group: 'system',
|
||||
key: '',
|
||||
name: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
group: "system",
|
||||
key: "",
|
||||
name: "",
|
||||
type: "string",
|
||||
value: "",
|
||||
options: null,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
description: '',
|
||||
is_system: false
|
||||
})
|
||||
optionsText.value = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
description: "",
|
||||
is_system: false,
|
||||
});
|
||||
optionsText.value = "";
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 监听record变化,初始化表单
|
||||
watch(
|
||||
@@ -258,102 +306,108 @@ watch(
|
||||
if (newRecord) {
|
||||
// 编辑模式
|
||||
Object.assign(formData, {
|
||||
group: newRecord.group || 'system',
|
||||
key: newRecord.key || '',
|
||||
name: newRecord.name || '',
|
||||
type: newRecord.type || 'string',
|
||||
group: newRecord.group || "system",
|
||||
key: newRecord.key || "",
|
||||
name: newRecord.name || "",
|
||||
type: newRecord.type || "string",
|
||||
value: parseValueByType(newRecord.value, newRecord.type),
|
||||
options: newRecord.options,
|
||||
sort: newRecord.sort || 0,
|
||||
status: newRecord.status ?? 1,
|
||||
description: newRecord.description || '',
|
||||
is_system: newRecord.is_system || false
|
||||
})
|
||||
description: newRecord.description || "",
|
||||
is_system: newRecord.is_system || false,
|
||||
});
|
||||
|
||||
// 解析选项
|
||||
if (newRecord.options && typeof newRecord.options === 'object') {
|
||||
if (newRecord.options && typeof newRecord.options === "object") {
|
||||
optionsText.value = newRecord.options
|
||||
.map((opt) => {
|
||||
if (typeof opt === 'object') {
|
||||
return `${opt.label}:${opt.value}`
|
||||
if (typeof opt === "object") {
|
||||
return `${opt.label}:${opt.value}`;
|
||||
}
|
||||
return opt
|
||||
return opt;
|
||||
})
|
||||
.join('\n')
|
||||
.join("\n");
|
||||
}
|
||||
} else {
|
||||
// 新增模式,重置表单
|
||||
resetForm()
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 添加自定义分组
|
||||
const handleAddCustomGroup = () => {
|
||||
if (!customGroup.value) {
|
||||
message.warning('请输入分组名称')
|
||||
return
|
||||
message.warning("请输入分组名称");
|
||||
return;
|
||||
}
|
||||
formData.group = customGroup.value
|
||||
customGroup.value = ''
|
||||
message.success('分组已添加')
|
||||
}
|
||||
formData.group = customGroup.value;
|
||||
customGroup.value = "";
|
||||
message.success("分组已添加");
|
||||
};
|
||||
|
||||
// 处理确定按钮
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
|
||||
const submitData = { ...formData }
|
||||
const submitData = { ...formData };
|
||||
|
||||
// 处理选项
|
||||
if (['select', 'radio', 'checkbox'].includes(formData.type) && optionsText.value) {
|
||||
submitData.options = parsedOptions.value
|
||||
if (
|
||||
["select", "radio", "checkbox"].includes(formData.type) &&
|
||||
optionsText.value
|
||||
) {
|
||||
submitData.options = parsedOptions.value;
|
||||
}
|
||||
|
||||
// 处理值类型转换
|
||||
if (formData.type === 'number') {
|
||||
submitData.value = Number(submitData.value)
|
||||
} else if (formData.type === 'boolean') {
|
||||
submitData.value = submitData.value ? '1' : '0'
|
||||
} else if (formData.type === 'checkbox' && Array.isArray(submitData.value)) {
|
||||
submitData.value = submitData.value.join(',')
|
||||
} else if (formData.type === 'json') {
|
||||
if (formData.type === "number") {
|
||||
submitData.value = Number(submitData.value);
|
||||
} else if (formData.type === "boolean") {
|
||||
submitData.value = submitData.value ? "1" : "0";
|
||||
} else if (
|
||||
formData.type === "checkbox" &&
|
||||
Array.isArray(submitData.value)
|
||||
) {
|
||||
submitData.value = submitData.value.join(",");
|
||||
} else if (formData.type === "json") {
|
||||
// 验证JSON格式
|
||||
try {
|
||||
JSON.parse(submitData.value)
|
||||
JSON.parse(submitData.value);
|
||||
} catch (e) {
|
||||
throw new Error('JSON格式不正确')
|
||||
throw new Error("JSON格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await systemApi.configs.edit.put(formData.id, submitData)
|
||||
message.success('更新成功')
|
||||
await systemApi.configs.edit.put(formData.id, submitData);
|
||||
message.success("更新成功");
|
||||
} else {
|
||||
await systemApi.configs.add.post(submitData)
|
||||
message.success('创建成功')
|
||||
await systemApi.configs.add.post(submitData);
|
||||
message.success("创建成功");
|
||||
}
|
||||
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
resetForm()
|
||||
emit("success");
|
||||
emit("update:visible", false);
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理取消按钮
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
resetForm();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -70,27 +70,57 @@
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">{{ getTypeText(record.type) }}</a-tag>
|
||||
<a-tag :color="getTypeColor(record.type)">{{
|
||||
getTypeText(record.type)
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'value'">
|
||||
<span v-if="['string', 'number', 'boolean'].includes(record.type)" class="value-text">
|
||||
<span
|
||||
v-if="
|
||||
['string', 'number', 'boolean'].includes(
|
||||
record.type,
|
||||
)
|
||||
"
|
||||
class="value-text"
|
||||
>
|
||||
{{ formatValue(record.value, record.type) }}
|
||||
</span>
|
||||
<span v-else-if="record.type === 'file'" class="file-value">
|
||||
<span
|
||||
v-else-if="record.type === 'file'"
|
||||
class="file-value"
|
||||
>
|
||||
<file-outlined />
|
||||
{{ record.value }}
|
||||
</span>
|
||||
<a v-else-if="record.type === 'image'" :href="record.value" target="_blank" class="image-value">
|
||||
<img :src="record.value" alt="预览" class="config-image" />
|
||||
<a
|
||||
v-else-if="record.type === 'image'"
|
||||
:href="record.value"
|
||||
target="_blank"
|
||||
class="image-value"
|
||||
>
|
||||
<img
|
||||
:src="record.value"
|
||||
alt="预览"
|
||||
class="config-image"
|
||||
/>
|
||||
</a>
|
||||
<span v-else class="json-value">{{ record.value }}</span>
|
||||
<span v-else class="json-value">{{
|
||||
record.value
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-badge :status="record.status ? 'success' : 'default'" :text="record.status ? '启用' : '禁用'" />
|
||||
<a-badge
|
||||
:status="record.status ? 'success' : 'default'"
|
||||
:text="record.status ? '启用' : '禁用'"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<edit-outlined />
|
||||
编辑
|
||||
</a-button>
|
||||
@@ -111,13 +141,17 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<SaveDialog v-model:visible="showSaveDialog" :record="currentRecord" @success="handleSaveSuccess" />
|
||||
<SaveDialog
|
||||
v-model:visible="showSaveDialog"
|
||||
:record="currentRecord"
|
||||
@success="handleSaveSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ref, reactive, onMounted, h } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
@@ -127,256 +161,272 @@ import {
|
||||
CheckOutlined,
|
||||
StopOutlined,
|
||||
EditOutlined,
|
||||
FileOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import systemApi from '@/api/system'
|
||||
import SaveDialog from './components/SaveDialog.vue'
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import scTable from "@/components/scTable/index.vue";
|
||||
import { useTable } from "@/hooks/useTable";
|
||||
import systemApi from "@/api/system";
|
||||
import SaveDialog from "./components/SaveDialog.vue";
|
||||
|
||||
// 表格引用
|
||||
const tableRef = ref(null)
|
||||
const tableRef = ref(null);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
group: undefined
|
||||
})
|
||||
keyword: "",
|
||||
group: undefined,
|
||||
});
|
||||
|
||||
// 分组选项
|
||||
const groupOptions = ref([])
|
||||
const groupOptions = ref([]);
|
||||
|
||||
// 当前记录
|
||||
const currentRecord = ref(null)
|
||||
const currentRecord = ref(null);
|
||||
|
||||
// 显示新增/编辑弹窗
|
||||
const showSaveDialog = ref(false)
|
||||
const showSaveDialog = ref(false);
|
||||
|
||||
// 使用 useTable Hook
|
||||
const { tableData, loading, pagination, rowSelection, handleSearch, handleReset, handlePaginationChange, refreshTable } =
|
||||
useTable({
|
||||
api: systemApi.configs.list.get,
|
||||
searchForm,
|
||||
needPagination: true
|
||||
})
|
||||
const {
|
||||
tableData,
|
||||
loading,
|
||||
pagination,
|
||||
rowSelection,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePaginationChange,
|
||||
refreshTable,
|
||||
} = useTable({
|
||||
api: systemApi.configs.list.get,
|
||||
searchForm,
|
||||
needPagination: true,
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '配置分组',
|
||||
dataIndex: 'group',
|
||||
key: 'group',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '配置键',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '配置名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '配置值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
ellipsis: true,
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
key: 'sort',
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
title: "配置分组",
|
||||
dataIndex: "group",
|
||||
key: "group",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "配置键",
|
||||
dataIndex: "key",
|
||||
key: "key",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "配置名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "配置值",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
ellipsis: true,
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
align: 'center'
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
title: "排序",
|
||||
dataIndex: "sort",
|
||||
key: "sort",
|
||||
width: 80,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
fixed: "right",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
string: 'blue',
|
||||
text: 'cyan',
|
||||
number: 'green',
|
||||
boolean: 'orange',
|
||||
select: 'purple',
|
||||
radio: 'purple',
|
||||
checkbox: 'purple',
|
||||
file: 'pink',
|
||||
json: 'geekblue'
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
string: "blue",
|
||||
text: "cyan",
|
||||
number: "green",
|
||||
boolean: "orange",
|
||||
select: "purple",
|
||||
radio: "purple",
|
||||
checkbox: "purple",
|
||||
file: "pink",
|
||||
json: "geekblue",
|
||||
};
|
||||
return colors[type] || "default";
|
||||
};
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
string: '字符串',
|
||||
text: '文本',
|
||||
number: '数字',
|
||||
boolean: '布尔值',
|
||||
select: '下拉框',
|
||||
radio: '单选框',
|
||||
checkbox: '多选框',
|
||||
file: '文件',
|
||||
json: 'JSON'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
string: "字符串",
|
||||
text: "文本",
|
||||
number: "数字",
|
||||
boolean: "布尔值",
|
||||
select: "下拉框",
|
||||
radio: "单选框",
|
||||
checkbox: "多选框",
|
||||
file: "文件",
|
||||
json: "JSON",
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 格式化值
|
||||
const formatValue = (value, type) => {
|
||||
if (type === 'boolean') {
|
||||
return value === 'true' || value === true ? '是' : '否'
|
||||
if (type === "boolean") {
|
||||
return value === "true" || value === true ? "是" : "否";
|
||||
}
|
||||
if (type === 'number') {
|
||||
return Number(value)
|
||||
if (type === "number") {
|
||||
return Number(value);
|
||||
}
|
||||
return value
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 获取分组列表
|
||||
const loadGroups = async () => {
|
||||
try {
|
||||
const res = await systemApi.configs.groups.get()
|
||||
groupOptions.value = res.data.map((item) => ({ label: item, value: item }))
|
||||
const res = await systemApi.configs.groups.get();
|
||||
groupOptions.value = res.data.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取分组列表失败:', error)
|
||||
console.error("获取分组列表失败:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
showSaveDialog.value = true
|
||||
}
|
||||
currentRecord.value = null;
|
||||
showSaveDialog.value = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record) => {
|
||||
currentRecord.value = { ...record }
|
||||
showSaveDialog.value = true
|
||||
}
|
||||
currentRecord.value = { ...record };
|
||||
showSaveDialog.value = true;
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (record) => {
|
||||
if (record.is_system) {
|
||||
message.warning('系统配置不能删除')
|
||||
return
|
||||
message.warning("系统配置不能删除");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定要删除配置"${record.name}"吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await systemApi.configs.delete.delete(record.id)
|
||||
message.success('删除成功')
|
||||
refreshTable()
|
||||
await systemApi.configs.delete.delete(record.id);
|
||||
message.success("删除成功");
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '删除失败')
|
||||
message.error(error.message || "删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys;
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要删除的配置')
|
||||
return
|
||||
message.warning("请先选择要删除的配置");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定要删除选中的 ${selectedRowKeys.length} 条配置吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await systemApi.configs.batchDelete.post({ ids: selectedRowKeys })
|
||||
message.success('批量删除成功')
|
||||
rowSelection.selectedRowKeys = []
|
||||
refreshTable()
|
||||
await systemApi.configs.batchDelete.post({
|
||||
ids: selectedRowKeys,
|
||||
});
|
||||
message.success("批量删除成功");
|
||||
rowSelection.selectedRowKeys = [];
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '批量删除失败')
|
||||
message.error(error.message || "批量删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量更新状态
|
||||
const handleBatchStatus = (status) => {
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys;
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要操作的配置')
|
||||
return
|
||||
message.warning("请先选择要操作的配置");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: status === 1 ? '确认启用' : '确认禁用',
|
||||
content: `确定要${status === 1 ? '启用' : '禁用'}选中的 ${selectedRowKeys.length} 条配置吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
title: status === 1 ? "确认启用" : "确认禁用",
|
||||
content: `确定要${status === 1 ? "启用" : "禁用"}选中的 ${selectedRowKeys.length} 条配置吗?`,
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await systemApi.configs.batchStatus.post({ ids: selectedRowKeys, status })
|
||||
message.success(`${status === 1 ? '启用' : '禁用'}成功`)
|
||||
rowSelection.selectedRowKeys = []
|
||||
refreshTable()
|
||||
await systemApi.configs.batchStatus.post({
|
||||
ids: selectedRowKeys,
|
||||
status,
|
||||
});
|
||||
message.success(`${status === 1 ? "启用" : "禁用"}成功`);
|
||||
rowSelection.selectedRowKeys = [];
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 保存成功
|
||||
const handleSaveSuccess = () => {
|
||||
showSaveDialog.value = false
|
||||
refreshTable()
|
||||
}
|
||||
showSaveDialog.value = false;
|
||||
refreshTable();
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadGroups()
|
||||
})
|
||||
loadGroups();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,43 +1,91 @@
|
||||
<template>
|
||||
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="600px">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-modal
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:confirm-loading="isSaving"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 字典名称 -->
|
||||
<a-form-item label="字典名称" name="name" required>
|
||||
<a-input v-model:value="form.name" placeholder="如:用户状态" allow-clear maxlength="50" show-count />
|
||||
<a-input
|
||||
v-model:value="form.name"
|
||||
placeholder="如:用户状态"
|
||||
allow-clear
|
||||
maxlength="50"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 字典编码 -->
|
||||
<a-form-item label="字典编码" name="code" required>
|
||||
<a-input v-model:value="form.code" placeholder="如:user_status" allow-clear :disabled="isEdit" />
|
||||
<div class="form-tip">系统唯一标识,只能包含字母、数字、下划线,且必须以字母开头</div>
|
||||
<a-input
|
||||
v-model:value="form.code"
|
||||
placeholder="如:user_status"
|
||||
allow-clear
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
系统唯一标识,只能包含字母、数字、下划线,且必须以字母开头
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 值类型 -->
|
||||
<a-form-item label="值类型" name="value_type" required>
|
||||
<a-select v-model:value="form.value_type" placeholder="请选择值类型" allow-clear>
|
||||
<a-select
|
||||
v-model:value="form.value_type"
|
||||
placeholder="请选择值类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="string">字符串</a-select-option>
|
||||
<a-select-option value="number">数字</a-select-option>
|
||||
<a-select-option value="boolean">布尔值</a-select-option>
|
||||
<a-select-option value="json">JSON</a-select-option>
|
||||
</a-select>
|
||||
<div class="form-tip">指定字典项值的类型,系统会根据类型自动格式化返回数据</div>
|
||||
<div class="form-tip">
|
||||
指定字典项值的类型,系统会根据类型自动格式化返回数据
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 排序 -->
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="form.sort"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">数值越小越靠前</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<a-form-item label="状态" name="status">
|
||||
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="dictionary_status" placeholder="请选择状态" allow-clear />
|
||||
<sc-select
|
||||
v-model:value="form.status"
|
||||
source-type="dictionary"
|
||||
dictionary-code="dictionary_status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 描述 -->
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="form.description" placeholder="请输入字典描述" :rows="3" maxlength="200"
|
||||
show-count />
|
||||
<a-textarea
|
||||
v-model:value="form.description"
|
||||
placeholder="请输入字典描述"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -45,129 +93,140 @@
|
||||
<div class="dialog-footer">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleSubmit"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import scSelect from '@/components/scSelect/index.vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import scSelect from "@/components/scSelect/index.vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
// ===== Props =====
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
dictionaryList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// ===== Emits =====
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
// ===== 状态 =====
|
||||
const formRef = ref(null)
|
||||
const isSaving = ref(false)
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
const formRef = ref(null);
|
||||
const isSaving = ref(false);
|
||||
const isEdit = computed(() => !!props.record?.id);
|
||||
|
||||
const title = computed(() => {
|
||||
return isEdit.value ? '编辑字典类型' : '新增字典类型'
|
||||
})
|
||||
return isEdit.value ? "编辑字典类型" : "新增字典类型";
|
||||
});
|
||||
|
||||
// ===== 表单数据 =====
|
||||
const form = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
value_type: 'string',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
code: "",
|
||||
value_type: "string",
|
||||
description: "",
|
||||
status: null,
|
||||
sort: 0
|
||||
})
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
// ===== 验证规则 =====
|
||||
// 编码唯一性验证
|
||||
const validateCodeUnique = async (rule, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!value) return Promise.resolve();
|
||||
|
||||
// 检查编码是否已存在(编辑时排除自己)
|
||||
const exists = props.dictionaryList.some(
|
||||
item => item.code === value && item.id !== props.record?.id
|
||||
)
|
||||
(item) => item.code === value && item.id !== props.record?.id,
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
return Promise.reject('该编码已存在,请使用其他编码')
|
||||
return Promise.reject("该编码已存在,请使用其他编码");
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '字典名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
{ required: true, message: "请输入字典名称", trigger: "blur" },
|
||||
{
|
||||
min: 2,
|
||||
max: 50,
|
||||
message: "字典名称长度在 2 到 50 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{ required: true, message: "请输入字典编码", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/,
|
||||
message: '编码格式不正确,只能包含字母、数字、下划线,且必须以字母开头',
|
||||
trigger: 'blur'
|
||||
message:
|
||||
"编码格式不正确,只能包含字母、数字、下划线,且必须以字母开头",
|
||||
trigger: "blur",
|
||||
},
|
||||
{ validator: validateCodeUnique, trigger: 'blur' }
|
||||
{ validator: validateCodeUnique, trigger: "blur" },
|
||||
],
|
||||
value_type: [
|
||||
{ required: true, message: '请选择值类型', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
{ required: true, message: "请选择值类型", trigger: "change" },
|
||||
],
|
||||
};
|
||||
|
||||
// ===== 方法:重置表单 =====
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
value_type: 'string',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
code: "",
|
||||
value_type: "string",
|
||||
description: "",
|
||||
status: null,
|
||||
sort: 0
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
sort: 0,
|
||||
};
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// ===== 方法:设置数据(编辑时) =====
|
||||
const setData = (data) => {
|
||||
if (data) {
|
||||
form.value = {
|
||||
id: data.id || '',
|
||||
name: data.name || '',
|
||||
code: data.code || '',
|
||||
value_type: data.value_type || 'string',
|
||||
description: data.description || '',
|
||||
id: data.id || "",
|
||||
name: data.name || "",
|
||||
code: data.code || "",
|
||||
value_type: data.value_type || "string",
|
||||
description: data.description || "",
|
||||
status: data.status !== undefined ? data.status : null,
|
||||
sort: data.sort !== undefined ? data.sort : 0
|
||||
}
|
||||
sort: data.sort !== undefined ? data.sort : 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:提交表单 =====
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await formRef.value.validate()
|
||||
await formRef.value.validate();
|
||||
|
||||
isSaving.value = true
|
||||
isSaving.value = true;
|
||||
|
||||
const submitData = {
|
||||
name: form.value.name,
|
||||
@@ -175,56 +234,63 @@ const handleSubmit = async () => {
|
||||
value_type: form.value.value_type,
|
||||
description: form.value.description,
|
||||
status: form.value.status,
|
||||
sort: form.value.sort
|
||||
}
|
||||
sort: form.value.sort,
|
||||
};
|
||||
|
||||
let res = {}
|
||||
let res = {};
|
||||
if (isEdit.value) {
|
||||
// 编辑
|
||||
res = await systemApi.dictionaries.edit.put(form.value.id, submitData)
|
||||
res = await systemApi.dictionaries.edit.put(
|
||||
form.value.id,
|
||||
submitData,
|
||||
);
|
||||
} else {
|
||||
// 新增
|
||||
res = await systemApi.dictionaries.add.post(submitData)
|
||||
res = await systemApi.dictionaries.add.post(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
message.success(isEdit.value ? "编辑成功" : "新增成功");
|
||||
emit("success");
|
||||
handleCancel();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证失败
|
||||
console.log('表单验证失败:', error)
|
||||
console.log("表单验证失败:", error);
|
||||
} else {
|
||||
// API 调用失败
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败')
|
||||
console.error("提交失败:", error);
|
||||
message.error("操作失败");
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:取消 =====
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
resetForm();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
// ===== 监听 visible 变化 =====
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
// 打开弹窗时,如果有 record 则设置数据
|
||||
if (props.record) {
|
||||
setData(props.record)
|
||||
} else {
|
||||
resetForm()
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 打开弹窗时,如果有 record 则设置数据
|
||||
if (props.record) {
|
||||
setData(props.record);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,50 +1,103 @@
|
||||
<template>
|
||||
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="600px">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-modal
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:confirm-loading="isSaving"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 标签名称 -->
|
||||
<a-form-item label="标签名称" name="label" required>
|
||||
<a-input v-model:value="form.label" placeholder="如:正常" allow-clear maxlength="50" show-count />
|
||||
<a-input
|
||||
v-model:value="form.label"
|
||||
placeholder="如:正常"
|
||||
allow-clear
|
||||
maxlength="50"
|
||||
show-count
|
||||
/>
|
||||
<div class="form-tip">用于前端显示的文本</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 数据值 -->
|
||||
<a-form-item label="数据值" name="value" required>
|
||||
<a-input v-model:value="form.value" placeholder="如:1" allow-clear />
|
||||
<a-input
|
||||
v-model:value="form.value"
|
||||
placeholder="如:1"
|
||||
allow-clear
|
||||
/>
|
||||
<div class="form-tip">实际使用的值,同一字典内必须唯一</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 颜色标记 -->
|
||||
<a-form-item label="颜色标记" name="color">
|
||||
<div style="display: flex; align-items: center">
|
||||
<input v-model="form.color" type="color" style="width: 60px; height: 32px; cursor: pointer" />
|
||||
<a-input v-model:value="form.color" placeholder="#1890ff" allow-clear
|
||||
style="flex: 1; margin-left: 10px" />
|
||||
<input
|
||||
v-model="form.color"
|
||||
type="color"
|
||||
style="width: 60px; height: 32px; cursor: pointer"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="form.color"
|
||||
placeholder="#1890ff"
|
||||
allow-clear
|
||||
style="flex: 1; margin-left: 10px"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-tip">用于前端展示的颜色标记</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 是否默认 -->
|
||||
<a-form-item label="默认项" name="is_default">
|
||||
<a-switch v-model:checked="isDefaultChecked" checked-children="是" un-checked-children="否" />
|
||||
<div class="form-tip" v-if="form.is_default" style="color: #faad14">
|
||||
<a-switch
|
||||
v-model:checked="isDefaultChecked"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
<div
|
||||
class="form-tip"
|
||||
v-if="form.is_default"
|
||||
style="color: #faad14"
|
||||
>
|
||||
设置为默认项后,同一字典内的其他默认项将自动取消
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 排序 -->
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="form.sort"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
|
||||
<a-switch
|
||||
v-model:checked="statusChecked"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 描述 -->
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="form.description" placeholder="请输入描述" :rows="3" maxlength="200"
|
||||
show-count />
|
||||
<a-textarea
|
||||
v-model:value="form.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -52,146 +105,156 @@
|
||||
<div class="dialog-footer">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleSubmit"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
// ===== Props =====
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
dictionaryId: {
|
||||
type: Number,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
itemList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// ===== Emits =====
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
// ===== 状态 =====
|
||||
const formRef = ref(null)
|
||||
const isSaving = ref(false)
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
const formRef = ref(null);
|
||||
const isSaving = ref(false);
|
||||
const isEdit = computed(() => !!props.record?.id);
|
||||
|
||||
const title = computed(() => {
|
||||
return isEdit.value ? '编辑字典项' : '新增字典项'
|
||||
})
|
||||
return isEdit.value ? "编辑字典项" : "新增字典项";
|
||||
});
|
||||
|
||||
// ===== 表单数据 =====
|
||||
const form = ref({
|
||||
id: '',
|
||||
label: '',
|
||||
value: '',
|
||||
color: '',
|
||||
description: '',
|
||||
id: "",
|
||||
label: "",
|
||||
value: "",
|
||||
color: "",
|
||||
description: "",
|
||||
is_default: false,
|
||||
status: true,
|
||||
sort: 0,
|
||||
dictionary_id: null
|
||||
})
|
||||
dictionary_id: null,
|
||||
});
|
||||
|
||||
// ===== 计算属性:状态开关 =====
|
||||
const statusChecked = computed({
|
||||
get: () => form.value.status === true,
|
||||
set: (val) => {
|
||||
form.value.status = val ? true : false
|
||||
}
|
||||
})
|
||||
form.value.status = val ? true : false;
|
||||
},
|
||||
});
|
||||
|
||||
// ===== 计算属性:默认项开关 =====
|
||||
const isDefaultChecked = computed({
|
||||
get: () => form.value.is_default === true,
|
||||
set: (val) => {
|
||||
form.value.is_default = val ? true : false
|
||||
}
|
||||
})
|
||||
form.value.is_default = val ? true : false;
|
||||
},
|
||||
});
|
||||
|
||||
// ===== 验证规则 =====
|
||||
// 数据值唯一性验证
|
||||
const validateValueUnique = async (rule, value) => {
|
||||
if (!value) return Promise.resolve()
|
||||
if (!value) return Promise.resolve();
|
||||
|
||||
// 检查数据值是否已存在(同一字典内,编辑时排除自己)
|
||||
const exists = props.itemList.some(
|
||||
item => item.value === value && item.id !== props.record?.id
|
||||
)
|
||||
(item) => item.value === value && item.id !== props.record?.id,
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
return Promise.reject('该数据值已存在,请使用其他值')
|
||||
return Promise.reject("该数据值已存在,请使用其他值");
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const rules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入标签名称', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '标签名称长度在 1 到 50 个字符', trigger: 'blur' }
|
||||
{ required: true, message: "请输入标签名称", trigger: "blur" },
|
||||
{
|
||||
min: 1,
|
||||
max: 50,
|
||||
message: "标签名称长度在 1 到 50 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入数据值', trigger: 'blur' },
|
||||
{ validator: validateValueUnique, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
{ required: true, message: "请输入数据值", trigger: "blur" },
|
||||
{ validator: validateValueUnique, trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// ===== 方法:重置表单 =====
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
id: '',
|
||||
label: '',
|
||||
value: '',
|
||||
color: '',
|
||||
description: '',
|
||||
id: "",
|
||||
label: "",
|
||||
value: "",
|
||||
color: "",
|
||||
description: "",
|
||||
is_default: false,
|
||||
status: true,
|
||||
sort: 0,
|
||||
dictionary_id: null
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
dictionary_id: null,
|
||||
};
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// ===== 方法:设置数据(编辑时) =====
|
||||
const setData = (data) => {
|
||||
if (data) {
|
||||
form.value = {
|
||||
id: data.id || '',
|
||||
label: data.label || '',
|
||||
value: data.value || '',
|
||||
color: data.color || '',
|
||||
description: data.description || '',
|
||||
id: data.id || "",
|
||||
label: data.label || "",
|
||||
value: data.value || "",
|
||||
color: data.color || "",
|
||||
description: data.description || "",
|
||||
is_default: data.is_default || false,
|
||||
status: data.status !== undefined ? data.status : true,
|
||||
sort: data.sort !== undefined ? data.sort : 0,
|
||||
dictionary_id: data.dictionary_id || props.dictionaryId
|
||||
}
|
||||
dictionary_id: data.dictionary_id || props.dictionaryId,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:提交表单 =====
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await formRef.value.validate()
|
||||
await formRef.value.validate();
|
||||
|
||||
isSaving.value = true
|
||||
isSaving.value = true;
|
||||
|
||||
const submitData = {
|
||||
label: form.value.label,
|
||||
@@ -201,58 +264,65 @@ const handleSubmit = async () => {
|
||||
is_default: form.value.is_default,
|
||||
status: form.value.status,
|
||||
sort: form.value.sort,
|
||||
dictionary_id: props.dictionaryId
|
||||
}
|
||||
dictionary_id: props.dictionaryId,
|
||||
};
|
||||
|
||||
let res = {}
|
||||
let res = {};
|
||||
if (isEdit.value) {
|
||||
// 编辑
|
||||
res = await systemApi.dictionaryItems.edit.put(form.value.id, submitData)
|
||||
res = await systemApi.dictionaryItems.edit.put(
|
||||
form.value.id,
|
||||
submitData,
|
||||
);
|
||||
} else {
|
||||
// 新增
|
||||
res = await systemApi.dictionaryItems.add.post(submitData)
|
||||
res = await systemApi.dictionaryItems.add.post(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
message.success(isEdit.value ? "编辑成功" : "新增成功");
|
||||
emit("success");
|
||||
handleCancel();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证失败
|
||||
console.log('表单验证失败:', error)
|
||||
console.log("表单验证失败:", error);
|
||||
} else {
|
||||
// API 调用失败
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败')
|
||||
console.error("提交失败:", error);
|
||||
message.error("操作失败");
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:取消 =====
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
resetForm();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
// ===== 监听 visible 变化 =====
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
// 打开弹窗时,如果有 record 则设置数据
|
||||
if (props.record) {
|
||||
setData(props.record)
|
||||
} else {
|
||||
resetForm()
|
||||
// 新增时设置 dictionary_id
|
||||
form.value.dictionary_id = props.dictionaryId
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 打开弹窗时,如果有 record 则设置数据
|
||||
if (props.record) {
|
||||
setData(props.record);
|
||||
} else {
|
||||
resetForm();
|
||||
// 新增时设置 dictionary_id
|
||||
form.value.dictionary_id = props.dictionaryId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,31 +3,55 @@
|
||||
<!-- 左侧:字典类型列表 -->
|
||||
<div class="left-box">
|
||||
<div class="header">
|
||||
<a-input v-model:value="dictionaryKeyword" placeholder="搜索字典..." allow-clear @change="handleDictionarySearch">
|
||||
<a-input
|
||||
v-model:value="dictionaryKeyword"
|
||||
placeholder="搜索字典..."
|
||||
allow-clear
|
||||
@change="handleDictionarySearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button type="primary" size="small" style="margin-top: 12px; width: 100%" @click="handleAddDictionary">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="margin-top: 12px; width: 100%"
|
||||
@click="handleAddDictionary"
|
||||
>
|
||||
<PlusOutlined /> 新增字典
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<!-- 字典列表 -->
|
||||
<div v-if="filteredDictionaries.length > 0" class="dictionary-list">
|
||||
<div v-for="item in filteredDictionaries" :key="item.id"
|
||||
:class="['dictionary-item', { 'active': selectedDictionaryId === item.id }]"
|
||||
@click="handleSelectDictionary(item)">
|
||||
<div
|
||||
v-if="filteredDictionaries.length > 0"
|
||||
class="dictionary-list"
|
||||
>
|
||||
<div
|
||||
v-for="item in filteredDictionaries"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'dictionary-item',
|
||||
{ active: selectedDictionaryId === item.id },
|
||||
]"
|
||||
@click="handleSelectDictionary(item)"
|
||||
>
|
||||
<div class="item-main">
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
<div class="item-code">{{ item.code }}</div>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<a-tag :color="item.status ? 'success' : 'default'" size="small">
|
||||
{{ item.status ? '启用' : '禁用' }}
|
||||
<a-tag
|
||||
:color="item.status ? 'success' : 'default'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.status ? "启用" : "禁用" }}
|
||||
</a-tag>
|
||||
<span class="item-count">{{ item.items_count || 0 }} 项</span>
|
||||
<span class="item-count"
|
||||
>{{ item.items_count || 0 }} 项</span
|
||||
>
|
||||
</div>
|
||||
<div class="item-actions" @click.stop>
|
||||
<a-dropdown>
|
||||
@@ -36,10 +60,17 @@
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="handleEditDictionary(item)">
|
||||
<a-menu-item
|
||||
@click="handleEditDictionary(item)"
|
||||
>
|
||||
<EditOutlined />编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="handleDeleteDictionary(item)" danger>
|
||||
<a-menu-item
|
||||
@click="
|
||||
handleDeleteDictionary(item)
|
||||
"
|
||||
danger
|
||||
>
|
||||
<DeleteOutlined />删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
@@ -50,7 +81,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-else-if="!dictionaryLoading" description="暂无字典类型" :image-size="80">
|
||||
<a-empty
|
||||
v-else-if="!dictionaryLoading"
|
||||
description="暂无字典类型"
|
||||
:image-size="80"
|
||||
>
|
||||
<a-button type="primary" @click="handleAddDictionary">
|
||||
创建第一个字典
|
||||
</a-button>
|
||||
@@ -64,11 +99,30 @@
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.label" placeholder="标签名称" allow-clear style="width: 140px" />
|
||||
<a-input v-model:value="searchForm.value" placeholder="数据值" allow-clear style="width: 140px" />
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 100px">
|
||||
<a-select-option :value="true">启用</a-select-option>
|
||||
<a-select-option :value="false">禁用</a-select-option>
|
||||
<a-input
|
||||
v-model:value="searchForm.label"
|
||||
placeholder="标签名称"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="searchForm.value"
|
||||
placeholder="数据值"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="状态"
|
||||
allow-clear
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option :value="true"
|
||||
>启用</a-select-option
|
||||
>
|
||||
<a-select-option :value="false"
|
||||
>禁用</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleItemSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
@@ -99,7 +153,11 @@
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button type="primary" :disabled="!selectedDictionaryId" @click="handleAddItem">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!selectedDictionaryId"
|
||||
@click="handleAddItem"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
@@ -110,15 +168,33 @@
|
||||
<div class="table-content">
|
||||
<!-- 空状态:未选择字典 -->
|
||||
<div v-if="!selectedDictionaryId" class="empty-state">
|
||||
<a-empty description="请选择左侧字典类型后操作" :image-size="120" />
|
||||
<a-empty
|
||||
description="请选择左侧字典类型后操作"
|
||||
:image-size="120"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 字典项表格 -->
|
||||
<scTable v-else ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
|
||||
:pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll">
|
||||
<scTable
|
||||
v-else
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="rowKey"
|
||||
:row-selection="rowSelection"
|
||||
@refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange"
|
||||
@select="handleSelectChange"
|
||||
@selectAll="handleSelectAll"
|
||||
>
|
||||
<template #color="{ record }">
|
||||
<span v-if="record.color" class="color-cell" :style="{ backgroundColor: record.color }"></span>
|
||||
<span
|
||||
v-if="record.color"
|
||||
class="color-cell"
|
||||
:style="{ backgroundColor: record.color }"
|
||||
></span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
@@ -131,17 +207,26 @@
|
||||
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status ? 'success' : 'error'">
|
||||
{{ record.status ? '启用' : '禁用' }}
|
||||
{{ record.status ? "启用" : "禁用" }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEditItem(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEditItem(record)"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定删除该字典项吗?" @confirm="handleDeleteItem(record)">
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该字典项吗?"
|
||||
@confirm="handleDeleteItem(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger
|
||||
>删除</a-button
|
||||
>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -151,17 +236,28 @@
|
||||
</div>
|
||||
|
||||
<!-- 字典类型弹窗 -->
|
||||
<DictionaryDialog v-if="dialog.dictionary" v-model:visible="dialog.dictionary" :record="currentDictionary"
|
||||
:dictionary-list="dictionaryList" @success="handleDictionarySuccess" />
|
||||
<DictionaryDialog
|
||||
v-if="dialog.dictionary"
|
||||
v-model:visible="dialog.dictionary"
|
||||
:record="currentDictionary"
|
||||
:dictionary-list="dictionaryList"
|
||||
@success="handleDictionarySuccess"
|
||||
/>
|
||||
|
||||
<!-- 字典项弹窗 -->
|
||||
<ItemDialog v-if="dialog.item" v-model:visible="dialog.item" :record="currentItem"
|
||||
:dictionary-id="selectedDictionaryId" :item-list="tableData" @success="handleItemSuccess" />
|
||||
<ItemDialog
|
||||
v-if="dialog.item"
|
||||
v-model:visible="dialog.item"
|
||||
:record="currentItem"
|
||||
:dictionary-id="selectedDictionaryId"
|
||||
:item-list="tableData"
|
||||
@success="handleItemSuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ref, reactive, onMounted, h } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
@@ -171,22 +267,22 @@ import {
|
||||
MoreOutlined,
|
||||
CheckCircleOutlined,
|
||||
StarOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import systemApi from '@/api/system'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
import DictionaryDialog from './components/DictionaryDialog.vue'
|
||||
import ItemDialog from './components/ItemDialog.vue'
|
||||
import { useDictionaryStore } from '@/stores/modules/dictionary'
|
||||
ExclamationCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useTable } from "@/hooks/useTable";
|
||||
import systemApi from "@/api/system";
|
||||
import scTable from "@/components/scTable/index.vue";
|
||||
import DictionaryDialog from "./components/DictionaryDialog.vue";
|
||||
import ItemDialog from "./components/ItemDialog.vue";
|
||||
import { useDictionaryStore } from "@/stores/modules/dictionary";
|
||||
|
||||
// ===== 字典列表相关 =====
|
||||
const dictionaryList = ref([])
|
||||
const filteredDictionaries = ref([])
|
||||
const selectedDictionary = ref(null)
|
||||
const selectedDictionaryId = ref(null)
|
||||
const dictionaryKeyword = ref('')
|
||||
const dictionaryLoading = ref(false)
|
||||
const dictionaryList = ref([]);
|
||||
const filteredDictionaries = ref([]);
|
||||
const selectedDictionary = ref(null);
|
||||
const selectedDictionaryId = ref(null);
|
||||
const dictionaryKeyword = ref("");
|
||||
const dictionaryLoading = ref(false);
|
||||
|
||||
// ===== 字典项相关(使用 useTable Hook)=====
|
||||
const {
|
||||
@@ -202,308 +298,367 @@ const {
|
||||
handlePaginationChange,
|
||||
handleSelectChange,
|
||||
handleSelectAll,
|
||||
refreshTable
|
||||
refreshTable,
|
||||
} = useTable({
|
||||
api: systemApi.dictionaryItems.list.get,
|
||||
searchForm: {
|
||||
dictionary_id: null,
|
||||
label: '',
|
||||
value: '',
|
||||
status: undefined
|
||||
label: "",
|
||||
value: "",
|
||||
status: undefined,
|
||||
},
|
||||
columns: [],
|
||||
needPagination: true,
|
||||
needSelection: true,
|
||||
immediateLoad: false // 不自动加载,等待选择字典
|
||||
})
|
||||
immediateLoad: false, // 不自动加载,等待选择字典
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
|
||||
{ title: '标签名称', dataIndex: 'label', key: 'label', width: 150, ellipsis: true },
|
||||
{ title: '数据值', dataIndex: 'value', key: 'value', width: 120, ellipsis: true },
|
||||
{ title: '颜色', dataIndex: 'color', key: 'color', width: 100, align: 'center', slot: 'color' },
|
||||
{ title: '默认项', dataIndex: 'is_default', key: 'is_default', width: 100, align: 'center', slot: 'is_default' },
|
||||
{ title: '排序', dataIndex: 'sort', key: 'sort', width: 80, align: 'center' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, align: 'center', slot: 'status' },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 150, align: 'center', fixed: 'right', slot: 'action' }
|
||||
]
|
||||
{ title: "ID", dataIndex: "id", key: "id", width: 80, align: "center" },
|
||||
{
|
||||
title: "标签名称",
|
||||
dataIndex: "label",
|
||||
key: "label",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "数据值",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "颜色",
|
||||
dataIndex: "color",
|
||||
key: "color",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "color",
|
||||
},
|
||||
{
|
||||
title: "默认项",
|
||||
dataIndex: "is_default",
|
||||
key: "is_default",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "is_default",
|
||||
},
|
||||
{
|
||||
title: "排序",
|
||||
dataIndex: "sort",
|
||||
key: "sort",
|
||||
width: 80,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "status",
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 150,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
slot: "action",
|
||||
},
|
||||
];
|
||||
|
||||
const rowKey = 'id'
|
||||
const rowKey = "id";
|
||||
|
||||
// ===== 弹窗状态 =====
|
||||
const dialog = reactive({
|
||||
dictionary: false,
|
||||
item: false
|
||||
})
|
||||
item: false,
|
||||
});
|
||||
|
||||
// ===== 当前操作的数据 =====
|
||||
const currentDictionary = ref(null)
|
||||
const currentItem = ref(null)
|
||||
const currentDictionary = ref(null);
|
||||
const currentItem = ref(null);
|
||||
|
||||
// ===== 方法:加载字典列表 =====
|
||||
const loadDictionaryList = async () => {
|
||||
try {
|
||||
dictionaryLoading.value = true
|
||||
const res = await systemApi.dictionaries.all.get()
|
||||
dictionaryLoading.value = true;
|
||||
const res = await systemApi.dictionaries.all.get();
|
||||
if (res.code === 200) {
|
||||
dictionaryList.value = res.data || []
|
||||
filteredDictionaries.value = res.data || []
|
||||
dictionaryList.value = res.data || [];
|
||||
filteredDictionaries.value = res.data || [];
|
||||
// 建立字典ID到Code的映射
|
||||
dictionaryStore.buildIdToCodeMap(res.data || [])
|
||||
dictionaryStore.buildIdToCodeMap(res.data || []);
|
||||
} else {
|
||||
message.error(res.message || '加载字典列表失败')
|
||||
message.error(res.message || "加载字典列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载字典列表失败:', error)
|
||||
message.error('加载字典列表失败')
|
||||
console.error("加载字典列表失败:", error);
|
||||
message.error("加载字典列表失败");
|
||||
} finally {
|
||||
dictionaryLoading.value = false
|
||||
dictionaryLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:搜索过滤字典 =====
|
||||
const handleDictionarySearch = (e) => {
|
||||
const keyword = e.target?.value || ''
|
||||
dictionaryKeyword.value = keyword
|
||||
const keyword = e.target?.value || "";
|
||||
dictionaryKeyword.value = keyword;
|
||||
|
||||
if (!keyword) {
|
||||
filteredDictionaries.value = dictionaryList.value
|
||||
return
|
||||
filteredDictionaries.value = dictionaryList.value;
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤字典列表(支持搜索名称和编码)
|
||||
filteredDictionaries.value = dictionaryList.value.filter(dict => {
|
||||
return dict.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
filteredDictionaries.value = dictionaryList.value.filter((dict) => {
|
||||
return (
|
||||
dict.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
dict.code.toLowerCase().includes(keyword.toLowerCase())
|
||||
})
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 方法:选择字典 =====
|
||||
const handleSelectDictionary = (dictionary) => {
|
||||
selectedDictionary.value = dictionary
|
||||
selectedDictionaryId.value = dictionary.id
|
||||
selectedDictionary.value = dictionary;
|
||||
selectedDictionaryId.value = dictionary.id;
|
||||
|
||||
// 重置右侧搜索条件
|
||||
searchForm.label = ''
|
||||
searchForm.value = ''
|
||||
searchForm.status = undefined
|
||||
searchForm.label = "";
|
||||
searchForm.value = "";
|
||||
searchForm.status = undefined;
|
||||
|
||||
// 更新 dictionary_id
|
||||
searchForm.dictionary_id = dictionary.id
|
||||
searchForm.dictionary_id = dictionary.id;
|
||||
|
||||
// 加载字典项列表
|
||||
handleItemSearch()
|
||||
}
|
||||
handleItemSearch();
|
||||
};
|
||||
|
||||
// ===== 方法:字典项搜索 =====
|
||||
const handleItemSearch = () => {
|
||||
if (!selectedDictionaryId.value) {
|
||||
message.warning('请先选择字典类型')
|
||||
return
|
||||
message.warning("请先选择字典类型");
|
||||
return;
|
||||
}
|
||||
searchForm.dictionary_id = selectedDictionaryId.value
|
||||
handleSearch()
|
||||
}
|
||||
searchForm.dictionary_id = selectedDictionaryId.value;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// ===== 方法:字典项重置 =====
|
||||
const handleItemReset = () => {
|
||||
searchForm.label = ''
|
||||
searchForm.value = ''
|
||||
searchForm.status = undefined
|
||||
searchForm.dictionary_id = selectedDictionaryId.value
|
||||
handleSearch()
|
||||
}
|
||||
searchForm.label = "";
|
||||
searchForm.value = "";
|
||||
searchForm.status = undefined;
|
||||
searchForm.dictionary_id = selectedDictionaryId.value;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// ===== 方法:新增字典 =====
|
||||
const handleAddDictionary = () => {
|
||||
currentDictionary.value = null
|
||||
dialog.dictionary = true
|
||||
}
|
||||
currentDictionary.value = null;
|
||||
dialog.dictionary = true;
|
||||
};
|
||||
|
||||
// ===== 方法:编辑字典 =====
|
||||
const handleEditDictionary = (dictionary) => {
|
||||
currentDictionary.value = { ...dictionary }
|
||||
dialog.dictionary = true
|
||||
}
|
||||
currentDictionary.value = { ...dictionary };
|
||||
dialog.dictionary = true;
|
||||
};
|
||||
|
||||
// ===== 方法:删除字典 =====
|
||||
const handleDeleteDictionary = (dictionary) => {
|
||||
const itemCount = dictionary.items_count || 0
|
||||
const itemCount = dictionary.items_count || 0;
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: itemCount > 0
|
||||
? `确定删除字典类型"${dictionary.name}"吗?删除后该字典下的 ${itemCount} 个字典项也会被删除!`
|
||||
: `确定删除字典类型"${dictionary.name}"吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
title: "确认删除",
|
||||
content:
|
||||
itemCount > 0
|
||||
? `确定删除字典类型"${dictionary.name}"吗?删除后该字典下的 ${itemCount} 个字典项也会被删除!`
|
||||
: `确定删除字典类型"${dictionary.name}"吗?`,
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
icon: h(ExclamationCircleOutlined),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await systemApi.dictionaries.delete.delete(dictionary.id)
|
||||
const res = await systemApi.dictionaries.delete.delete(
|
||||
dictionary.id,
|
||||
);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
message.success("删除成功");
|
||||
|
||||
// 如果删除的是当前选中的字典,清空右侧
|
||||
if (selectedDictionaryId.value === dictionary.id) {
|
||||
selectedDictionary.value = null
|
||||
selectedDictionaryId.value = null
|
||||
tableData.value = []
|
||||
selectedDictionary.value = null;
|
||||
selectedDictionaryId.value = null;
|
||||
tableData.value = [];
|
||||
}
|
||||
|
||||
// 刷新字典列表
|
||||
await loadDictionaryList()
|
||||
await loadDictionaryList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
message.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除字典失败:', error)
|
||||
message.error('删除失败')
|
||||
console.error("删除字典失败:", error);
|
||||
message.error("删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 方法:新增字典项 =====
|
||||
const handleAddItem = () => {
|
||||
if (!selectedDictionaryId.value) {
|
||||
message.warning('请先选择字典类型')
|
||||
return
|
||||
message.warning("请先选择字典类型");
|
||||
return;
|
||||
}
|
||||
|
||||
currentItem.value = null
|
||||
dialog.item = true
|
||||
}
|
||||
currentItem.value = null;
|
||||
dialog.item = true;
|
||||
};
|
||||
|
||||
// ===== 方法:编辑字典项 =====
|
||||
const handleEditItem = (record) => {
|
||||
currentItem.value = { ...record }
|
||||
dialog.item = true
|
||||
}
|
||||
currentItem.value = { ...record };
|
||||
dialog.item = true;
|
||||
};
|
||||
|
||||
// ===== 方法:删除字典项 =====
|
||||
const handleDeleteItem = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.dictionaryItems.delete.delete(record.id)
|
||||
const res = await systemApi.dictionaryItems.delete.delete(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
message.success("删除成功");
|
||||
// 清除字典缓存
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value)
|
||||
refreshTable()
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value);
|
||||
refreshTable();
|
||||
// 刷新字典列表以更新项数量
|
||||
loadDictionaryList()
|
||||
loadDictionaryList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
message.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除字典项失败:', error)
|
||||
message.error('删除失败')
|
||||
console.error("删除字典项失败:", error);
|
||||
message.error("删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:批量删除字典项 =====
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要删除的字典项')
|
||||
return
|
||||
message.warning("请选择要删除的字典项");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定删除选中的 ${selectedRows.value.length} 个字典项吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
okText: "删除",
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
icon: h(ExclamationCircleOutlined),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const res = await systemApi.dictionaryItems.batchDelete.post({ ids })
|
||||
const ids = selectedRows.value.map((item) => item.id);
|
||||
const res = await systemApi.dictionaryItems.batchDelete.post({
|
||||
ids,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
message.success("删除成功");
|
||||
// 清除字典缓存
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value)
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
loadDictionaryList()
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value);
|
||||
selectedRows.value = [];
|
||||
refreshTable();
|
||||
loadDictionaryList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
message.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
message.error('删除失败')
|
||||
console.error("批量删除失败:", error);
|
||||
message.error("删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 方法:批量启用/禁用字典项 =====
|
||||
const handleBatchStatus = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要操作的字典项')
|
||||
return
|
||||
message.warning("请选择要操作的字典项");
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatus = selectedRows.value[0].status ? false : true
|
||||
const statusText = newStatus === 1 ? '启用' : '禁用'
|
||||
const newStatus = selectedRows.value[0].status ? false : true;
|
||||
const statusText = newStatus === 1 ? "启用" : "禁用";
|
||||
|
||||
Modal.confirm({
|
||||
title: `确认${statusText}`,
|
||||
content: `确定要${statusText}选中的 ${selectedRows.value.length} 个字典项吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const ids = selectedRows.value.map((item) => item.id);
|
||||
const res = await systemApi.dictionaryItems.batchStatus.post({
|
||||
ids,
|
||||
status: newStatus
|
||||
})
|
||||
status: newStatus,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success(`${statusText}成功`)
|
||||
message.success(`${statusText}成功`);
|
||||
// 清除字典缓存
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value)
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value);
|
||||
selectedRows.value = [];
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error)
|
||||
message.error('操作失败')
|
||||
console.error("批量操作失败:", error);
|
||||
message.error("操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化字典 store
|
||||
const dictionaryStore = useDictionaryStore()
|
||||
const dictionaryStore = useDictionaryStore();
|
||||
|
||||
// ===== 方法:字典操作成功回调 =====
|
||||
const handleDictionarySuccess = () => {
|
||||
dialog.dictionary = false
|
||||
dialog.dictionary = false;
|
||||
// 清理字典缓存
|
||||
dictionaryStore.clearCache()
|
||||
loadDictionaryList()
|
||||
}
|
||||
dictionaryStore.clearCache();
|
||||
loadDictionaryList();
|
||||
};
|
||||
|
||||
// ===== 方法:字典项操作成功回调 =====
|
||||
const handleItemSuccess = () => {
|
||||
dialog.item = false
|
||||
dialog.item = false;
|
||||
// 清理字典缓存
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value)
|
||||
refreshTable()
|
||||
loadDictionaryList()
|
||||
}
|
||||
dictionaryStore.clearDictionary(selectedDictionaryId.value);
|
||||
refreshTable();
|
||||
loadDictionaryList();
|
||||
};
|
||||
|
||||
// ===== 生命周期 =====
|
||||
onMounted(() => {
|
||||
// 加载字典列表
|
||||
loadDictionaryList()
|
||||
})
|
||||
loadDictionaryList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -557,7 +712,7 @@ onMounted(() => {
|
||||
.item-code {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="通知标题/内容" allow-clear style="width: 180px" />
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="通知标题/内容"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="searchForm.is_read"
|
||||
placeholder="阅读状态"
|
||||
@@ -39,7 +44,11 @@
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<a-space>
|
||||
<a-badge :count="unreadCount" :offset="[-5, 5]" :number-style="{ backgroundColor: '#f5222d' }">
|
||||
<a-badge
|
||||
:count="unreadCount"
|
||||
:offset="[-5, 5]"
|
||||
:number-style="{ backgroundColor: '#f5222d' }"
|
||||
>
|
||||
<a-button @click="handleMarkAllRead">
|
||||
<template #icon><check-outlined /></template>
|
||||
全部已读
|
||||
@@ -85,29 +94,48 @@
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
<div class="notification-title" :class="{ unread: !record.is_read }" @click="handleViewDetail(record)">
|
||||
<WarningOutlined v-if="!record.is_read" class="unread-icon" />
|
||||
<div
|
||||
class="notification-title"
|
||||
:class="{ unread: !record.is_read }"
|
||||
@click="handleViewDetail(record)"
|
||||
>
|
||||
<WarningOutlined
|
||||
v-if="!record.is_read"
|
||||
class="unread-icon"
|
||||
/>
|
||||
<span>{{ record.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
<component :is="getTypeIcon(record.type)" class="type-icon" />
|
||||
<component
|
||||
:is="getTypeIcon(record.type)"
|
||||
class="type-icon"
|
||||
/>
|
||||
{{ getTypeText(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'category'">
|
||||
<a-tag color="blue">{{ getCategoryText(record.category) }}</a-tag>
|
||||
<a-tag color="blue">{{
|
||||
getCategoryText(record.category)
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'is_read'">
|
||||
<a-badge :status="record.is_read ? 'default' : 'processing'" :text="record.is_read ? '已读' : '未读'" />
|
||||
<a-badge
|
||||
:status="record.is_read ? 'default' : 'processing'"
|
||||
:text="record.is_read ? '已读' : '未读'"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'created_at'">
|
||||
<span>{{ formatTime(record.created_at) }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleViewDetail(record)"
|
||||
>
|
||||
<eye-outlined />
|
||||
查看
|
||||
</a-button>
|
||||
@@ -120,7 +148,12 @@
|
||||
<check-outlined />
|
||||
标为已读
|
||||
</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDelete(record)"
|
||||
>
|
||||
<delete-outlined />
|
||||
删除
|
||||
</a-button>
|
||||
@@ -131,7 +164,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 通知详情弹窗 -->
|
||||
<a-drawer v-model:open="showDetailDrawer" title="通知详情" placement="right" width="600">
|
||||
<a-drawer
|
||||
v-model:open="showDetailDrawer"
|
||||
title="通知详情"
|
||||
placement="right"
|
||||
width="600"
|
||||
>
|
||||
<template v-if="currentNotification">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="标题">
|
||||
@@ -139,38 +177,68 @@
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="类型">
|
||||
<a-tag :color="getTypeColor(currentNotification.type)">
|
||||
<component :is="getTypeIcon(currentNotification.type)" class="type-icon" />
|
||||
<component
|
||||
:is="getTypeIcon(currentNotification.type)"
|
||||
class="type-icon"
|
||||
/>
|
||||
{{ getTypeText(currentNotification.type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="分类">
|
||||
<a-tag color="blue">{{ getCategoryText(currentNotification.category) }}</a-tag>
|
||||
<a-tag color="blue">{{
|
||||
getCategoryText(currentNotification.category)
|
||||
}}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-badge
|
||||
:status="currentNotification.is_read ? 'default' : 'processing'"
|
||||
:text="currentNotification.is_read ? '已读' : '未读'"
|
||||
:status="
|
||||
currentNotification.is_read
|
||||
? 'default'
|
||||
: 'processing'
|
||||
"
|
||||
:text="
|
||||
currentNotification.is_read ? '已读' : '未读'
|
||||
"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatTime(currentNotification.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentNotification.read_at" label="阅读时间">
|
||||
<a-descriptions-item
|
||||
v-if="currentNotification.read_at"
|
||||
label="阅读时间"
|
||||
>
|
||||
{{ formatTime(currentNotification.read_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="notification-content">
|
||||
<div class="content-label">通知内容:</div>
|
||||
<div class="content-text">{{ currentNotification.content }}</div>
|
||||
<div class="content-text">
|
||||
{{ currentNotification.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentNotification.data && Object.keys(currentNotification.data).length > 0" class="notification-data">
|
||||
<div
|
||||
v-if="
|
||||
currentNotification.data &&
|
||||
Object.keys(currentNotification.data).length > 0
|
||||
"
|
||||
class="notification-data"
|
||||
>
|
||||
<div class="data-label">附加数据:</div>
|
||||
<pre class="data-text">{{ JSON.stringify(currentNotification.data, null, 2) }}</pre>
|
||||
<pre class="data-text">{{
|
||||
JSON.stringify(currentNotification.data, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="currentNotification.action_type && currentNotification.action_type !== 'none'" class="notification-action">
|
||||
<div
|
||||
v-if="
|
||||
currentNotification.action_type &&
|
||||
currentNotification.action_type !== 'none'
|
||||
"
|
||||
class="notification-action"
|
||||
>
|
||||
<a-button type="primary" @click="handleAction">
|
||||
<template #icon><arrow-right-outlined /></template>
|
||||
{{ getActionText(currentNotification.action_type) }}
|
||||
@@ -182,8 +250,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, h } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, h } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
@@ -200,120 +268,127 @@ import {
|
||||
MessageOutlined,
|
||||
ClockCircleOutlined,
|
||||
BulbOutlined,
|
||||
ArrowRightOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import systemApi from '@/api/system'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
ArrowRightOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import scTable from "@/components/scTable/index.vue";
|
||||
import { useTable } from "@/hooks/useTable";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
// 表格引用
|
||||
const tableRef = ref(null)
|
||||
const tableRef = ref(null);
|
||||
|
||||
// WebSocket
|
||||
const ws = useWebSocket()
|
||||
let unreadCountInterval = null
|
||||
// 注意:WebSocket 已在 App.vue 中统一初始化,这里不需要重复连接
|
||||
// 通知会通过 notification store 自动更新
|
||||
let unreadCountInterval = null;
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
keyword: "",
|
||||
is_read: undefined,
|
||||
type: undefined,
|
||||
category: undefined
|
||||
})
|
||||
category: undefined,
|
||||
});
|
||||
|
||||
// 未读数量
|
||||
const unreadCount = ref(0)
|
||||
const unreadCount = ref(0);
|
||||
|
||||
// 当前通知
|
||||
const currentNotification = ref(null)
|
||||
const currentNotification = ref(null);
|
||||
|
||||
// 显示详情抽屉
|
||||
const showDetailDrawer = ref(false)
|
||||
const showDetailDrawer = ref(false);
|
||||
|
||||
// 通知类型选项
|
||||
const typeOptions = [
|
||||
{ label: '信息', value: 'info' },
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '错误', value: 'error' },
|
||||
{ label: '任务', value: 'task' },
|
||||
{ label: '系统', value: 'system' }
|
||||
]
|
||||
{ label: "信息", value: "info" },
|
||||
{ label: "成功", value: "success" },
|
||||
{ label: "警告", value: "warning" },
|
||||
{ label: "错误", value: "error" },
|
||||
{ label: "任务", value: "task" },
|
||||
{ label: "系统", value: "system" },
|
||||
];
|
||||
|
||||
// 通知分类选项
|
||||
const categoryOptions = [
|
||||
{ label: '系统通知', value: 'system' },
|
||||
{ label: '任务通知', value: 'task' },
|
||||
{ label: '消息通知', value: 'message' },
|
||||
{ label: '提醒通知', value: 'reminder' },
|
||||
{ label: '公告通知', value: 'announcement' }
|
||||
]
|
||||
{ label: "系统通知", value: "system" },
|
||||
{ label: "任务通知", value: "task" },
|
||||
{ label: "消息通知", value: "message" },
|
||||
{ label: "提醒通知", value: "reminder" },
|
||||
{ label: "公告通知", value: "announcement" },
|
||||
];
|
||||
|
||||
// 使用 useTable Hook
|
||||
const { tableData, loading, pagination, rowSelection, handleSearch, handleReset, handlePaginationChange, refreshTable } =
|
||||
useTable({
|
||||
api: systemApi.notifications.list.get,
|
||||
searchForm,
|
||||
needPagination: true
|
||||
})
|
||||
const {
|
||||
tableData,
|
||||
loading,
|
||||
pagination,
|
||||
rowSelection,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePaginationChange,
|
||||
refreshTable,
|
||||
} = useTable({
|
||||
api: systemApi.notifications.list.get,
|
||||
searchForm,
|
||||
needPagination: true,
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '通知标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
title: "通知标题",
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
width: 300,
|
||||
ellipsis: true
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
title: "类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 120,
|
||||
align: 'center'
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
title: "分类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 120,
|
||||
align: 'center'
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_read',
|
||||
key: 'is_read',
|
||||
title: "状态",
|
||||
dataIndex: "is_read",
|
||||
key: "is_read",
|
||||
width: 100,
|
||||
align: 'center'
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
},
|
||||
];
|
||||
|
||||
// 获取类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
info: 'blue',
|
||||
success: 'green',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
task: 'purple',
|
||||
system: 'cyan'
|
||||
}
|
||||
return colors[type] || 'default'
|
||||
}
|
||||
info: "blue",
|
||||
success: "green",
|
||||
warning: "orange",
|
||||
error: "red",
|
||||
task: "purple",
|
||||
system: "cyan",
|
||||
};
|
||||
return colors[type] || "default";
|
||||
};
|
||||
|
||||
// 获取类型图标
|
||||
const getTypeIcon = (type) => {
|
||||
@@ -323,251 +398,255 @@ const getTypeIcon = (type) => {
|
||||
warning: ExclamationCircleOutlined,
|
||||
error: CloseCircleOutlined,
|
||||
task: BellOutlined,
|
||||
system: MessageOutlined
|
||||
}
|
||||
return icons[type] || InfoCircleOutlined
|
||||
}
|
||||
system: MessageOutlined,
|
||||
};
|
||||
return icons[type] || InfoCircleOutlined;
|
||||
};
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
info: '信息',
|
||||
success: '成功',
|
||||
warning: '警告',
|
||||
error: '错误',
|
||||
task: '任务',
|
||||
system: '系统'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
info: "信息",
|
||||
success: "成功",
|
||||
warning: "警告",
|
||||
error: "错误",
|
||||
task: "任务",
|
||||
system: "系统",
|
||||
};
|
||||
return texts[type] || type;
|
||||
};
|
||||
|
||||
// 获取分类文本
|
||||
const getCategoryText = (category) => {
|
||||
const texts = {
|
||||
system: '系统通知',
|
||||
task: '任务通知',
|
||||
message: '消息通知',
|
||||
reminder: '提醒通知',
|
||||
announcement: '公告通知'
|
||||
}
|
||||
return texts[category] || category
|
||||
}
|
||||
system: "系统通知",
|
||||
task: "任务通知",
|
||||
message: "消息通知",
|
||||
reminder: "提醒通知",
|
||||
announcement: "公告通知",
|
||||
};
|
||||
return texts[category] || category;
|
||||
};
|
||||
|
||||
// 获取操作文本
|
||||
const getActionText = (actionType) => {
|
||||
const texts = {
|
||||
link: '查看详情',
|
||||
modal: '打开弹窗',
|
||||
none: ''
|
||||
}
|
||||
return texts[actionType] || '查看'
|
||||
}
|
||||
link: "查看详情",
|
||||
modal: "打开弹窗",
|
||||
none: "",
|
||||
};
|
||||
return texts[actionType] || "查看";
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
if (!time) return "-";
|
||||
return new Date(time).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// 获取未读数量
|
||||
const loadUnreadCount = async () => {
|
||||
try {
|
||||
const res = await systemApi.notifications.unreadCount.get()
|
||||
unreadCount.value = res.data.count
|
||||
const res = await systemApi.notifications.unreadCount.get();
|
||||
unreadCount.value = res.data.count;
|
||||
} catch (error) {
|
||||
console.error('获取未读数量失败:', error)
|
||||
console.error("获取未读数量失败:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (record) => {
|
||||
currentNotification.value = { ...record }
|
||||
showDetailDrawer.value = true
|
||||
currentNotification.value = { ...record };
|
||||
showDetailDrawer.value = true;
|
||||
|
||||
// 自动标记为已读
|
||||
if (!record.is_read) {
|
||||
await handleMarkRead(record)
|
||||
await handleMarkRead(record);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 标记已读
|
||||
const handleMarkRead = async (record) => {
|
||||
try {
|
||||
await systemApi.notifications.markAsRead.post(record.id)
|
||||
message.success('已标记为已读')
|
||||
await systemApi.notifications.markAsRead.post(record.id);
|
||||
message.success("已标记为已读");
|
||||
if (!record.is_read) {
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||
}
|
||||
refreshTable()
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 批量标记已读
|
||||
const handleBatchMarkRead = () => {
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys;
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要操作的通知')
|
||||
return
|
||||
message.warning("请先选择要操作的通知");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认标记为已读',
|
||||
title: "确认标记为已读",
|
||||
content: `确定要将选中的 ${selectedRowKeys.length} 条通知标记为已读吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await systemApi.notifications.batchMarkAsRead.post({ ids: selectedRowKeys })
|
||||
message.success('批量标记成功')
|
||||
rowSelection.selectedRowKeys = []
|
||||
unreadCount.value = Math.max(0, unreadCount.value - res.data.count)
|
||||
refreshTable()
|
||||
const res = await systemApi.notifications.batchMarkAsRead.post({
|
||||
ids: selectedRowKeys,
|
||||
});
|
||||
message.success("批量标记成功");
|
||||
rowSelection.selectedRowKeys = [];
|
||||
unreadCount.value = Math.max(
|
||||
0,
|
||||
unreadCount.value - res.data.count,
|
||||
);
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 标记全部已读
|
||||
const handleMarkAllRead = () => {
|
||||
Modal.confirm({
|
||||
title: '确认全部已读',
|
||||
content: '确定要将所有未读通知标记为已读吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
title: "确认全部已读",
|
||||
content: "确定要将所有未读通知标记为已读吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await systemApi.notifications.markAllAsRead.post()
|
||||
message.success('已标记全部为已读')
|
||||
unreadCount.value = 0
|
||||
refreshTable()
|
||||
const res = await systemApi.notifications.markAllAsRead.post();
|
||||
message.success("已标记全部为已读");
|
||||
unreadCount.value = 0;
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定要删除通知"${record.title}"吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await systemApi.notifications.delete.delete(record.id)
|
||||
message.success('删除成功')
|
||||
await systemApi.notifications.delete.delete(record.id);
|
||||
message.success("删除成功");
|
||||
if (!record.is_read) {
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||
}
|
||||
refreshTable()
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '删除失败')
|
||||
message.error(error.message || "删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys
|
||||
const selectedRowKeys = rowSelection.selectedRowKeys;
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要删除的通知')
|
||||
return
|
||||
message.warning("请先选择要删除的通知");
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定要删除选中的 ${selectedRowKeys.length} 条通知吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await systemApi.notifications.batchDelete.post({ ids: selectedRowKeys })
|
||||
message.success('批量删除成功')
|
||||
rowSelection.selectedRowKeys = []
|
||||
loadUnreadCount()
|
||||
refreshTable()
|
||||
const res = await systemApi.notifications.batchDelete.post({
|
||||
ids: selectedRowKeys,
|
||||
});
|
||||
message.success("批量删除成功");
|
||||
rowSelection.selectedRowKeys = [];
|
||||
loadUnreadCount();
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '批量删除失败')
|
||||
message.error(error.message || "批量删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 清空已读
|
||||
const handleClearRead = () => {
|
||||
Modal.confirm({
|
||||
title: '确认清空',
|
||||
content: '确定要清空所有已读通知吗?此操作不可恢复。',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
title: "确认清空",
|
||||
content: "确定要清空所有已读通知吗?此操作不可恢复。",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: async () => {
|
||||
try {
|
||||
await systemApi.notifications.clearRead.post()
|
||||
message.success('已清空已读通知')
|
||||
refreshTable()
|
||||
await systemApi.notifications.clearRead.post();
|
||||
message.success("已清空已读通知");
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
message.error(error.message || '操作失败')
|
||||
message.error(error.message || "操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 处理操作
|
||||
const handleAction = () => {
|
||||
const notification = currentNotification.value
|
||||
if (!notification) return
|
||||
const notification = currentNotification.value;
|
||||
if (!notification) return;
|
||||
|
||||
if (notification.action_type === 'link' && notification.action_data?.url) {
|
||||
window.open(notification.action_data.url, '_blank')
|
||||
} else if (notification.action_type === 'modal') {
|
||||
if (notification.action_type === "link" && notification.action_data?.url) {
|
||||
window.open(notification.action_data.url, "_blank");
|
||||
} else if (notification.action_type === "modal") {
|
||||
// 打开弹窗的逻辑
|
||||
message.info('打开弹窗功能')
|
||||
message.info("打开弹窗功能");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// WebSocket 消息处理
|
||||
const handleWebSocketMessage = (msg) => {
|
||||
if (msg.type === 'notification') {
|
||||
if (msg.type === "notification") {
|
||||
// 收到新通知
|
||||
const notification = msg.data
|
||||
message.info(`新通知: ${notification.title}`)
|
||||
unreadCount.value++
|
||||
refreshTable()
|
||||
const notification = msg.data;
|
||||
message.info(`新通知: ${notification.title}`);
|
||||
unreadCount.value++;
|
||||
refreshTable();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadUnreadCount()
|
||||
|
||||
// 连接 WebSocket
|
||||
ws.connect()
|
||||
ws.onMessage(handleWebSocketMessage)
|
||||
loadUnreadCount();
|
||||
|
||||
// 定时刷新未读数量(每30秒)
|
||||
// 注意:通知也会通过 WebSocket 实时推送,这是作为备用刷新机制
|
||||
unreadCountInterval = setInterval(() => {
|
||||
loadUnreadCount()
|
||||
}, 30000)
|
||||
})
|
||||
loadUnreadCount();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
if (unreadCountInterval) {
|
||||
clearInterval(unreadCountInterval)
|
||||
clearInterval(unreadCountInterval);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<template>
|
||||
<a-modal title="任务详情" :open="visible" :footer="null" @cancel="handleCancel" width="800px">
|
||||
<a-modal
|
||||
title="任务详情"
|
||||
:open="visible"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
width="800px"
|
||||
>
|
||||
<a-descriptions bordered :column="2" v-if="task">
|
||||
<a-descriptions-item label="任务名称">{{ task.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="任务名称">{{
|
||||
task.name
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="任务类型">
|
||||
<a-tag :color="getTypeColor(task.type)">{{ getTypeText(task.type) }}</a-tag>
|
||||
<a-tag :color="getTypeColor(task.type)">{{
|
||||
getTypeText(task.type)
|
||||
}}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="命令/类" :span="2">
|
||||
<code class="command-code">{{ task.command }}</code>
|
||||
@@ -11,17 +21,19 @@
|
||||
<a-descriptions-item label="Cron表达式">
|
||||
<code class="cron-code">{{ task.expression }}</code>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="时区">{{ task.timezone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="时区">{{
|
||||
task.timezone
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态" :span="2">
|
||||
<a-tag :color="task.is_active ? 'success' : 'error'">
|
||||
{{ task.is_active ? '启用' : '禁用' }}
|
||||
{{ task.is_active ? "启用" : "禁用" }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="上次运行时间" :span="2">
|
||||
{{ task.last_run_at ? formatDate(task.last_run_at) : '未运行' }}
|
||||
{{ task.last_run_at ? formatDate(task.last_run_at) : "未运行" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="下次运行时间" :span="2">
|
||||
{{ task.next_run_at ? formatDate(task.next_run_at) : '-' }}
|
||||
{{ task.next_run_at ? formatDate(task.next_run_at) : "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行次数">
|
||||
<a-tag color="success">成功: {{ task.run_count || 0 }}</a-tag>
|
||||
@@ -32,10 +44,10 @@
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="后台运行" :span="2">
|
||||
{{ task.run_in_background ? '是' : '否' }}
|
||||
{{ task.run_in_background ? "是" : "否" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ task.description || '-' }}
|
||||
{{ task.description || "-" }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
@@ -59,75 +71,75 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, computed } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { PlayCircleOutlined } from "@ant-design/icons-vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'refresh'])
|
||||
const emit = defineEmits(["update:visible", "refresh"]);
|
||||
|
||||
const task = computed(() => props.record)
|
||||
const task = computed(() => props.record);
|
||||
|
||||
// 获取任务类型文本
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
command: '命令',
|
||||
job: '任务',
|
||||
closure: '闭包'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
command: "命令",
|
||||
job: "任务",
|
||||
closure: "闭包",
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// 获取任务类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
command: 'blue',
|
||||
job: 'green',
|
||||
closure: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
command: "blue",
|
||||
job: "green",
|
||||
closure: "orange",
|
||||
};
|
||||
return colorMap[type] || "default";
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
if (!dateStr) return "-";
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString("zh-CN");
|
||||
};
|
||||
|
||||
// 执行任务
|
||||
const handleRun = async () => {
|
||||
if (!props.record) return
|
||||
if (!props.record) return;
|
||||
|
||||
try {
|
||||
const res = await systemApi.tasks.run.post(props.record.id)
|
||||
const res = await systemApi.tasks.run.post(props.record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('任务执行成功')
|
||||
emit('refresh')
|
||||
handleCancel()
|
||||
message.success("任务执行成功");
|
||||
emit("refresh");
|
||||
handleCancel();
|
||||
} else {
|
||||
message.error(res.message || '任务执行失败')
|
||||
message.error(res.message || "任务执行失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('任务执行失败')
|
||||
message.error("任务执行失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
emit("update:visible", false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -135,7 +147,7 @@ const handleCancel = () => {
|
||||
padding: 4px 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -144,7 +156,7 @@ const handleCancel = () => {
|
||||
padding: 2px 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -157,7 +169,7 @@ const handleCancel = () => {
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
<template>
|
||||
<a-modal :title="title" :open="visible" :confirm-loading="isSaving" :footer="null" @cancel="handleCancel" width="700px">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||||
<a-modal
|
||||
:title="title"
|
||||
:open="visible"
|
||||
:confirm-loading="isSaving"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
width="700px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<!-- 任务名称 -->
|
||||
<a-form-item label="任务名称" name="name" required>
|
||||
<a-input v-model:value="form.name" placeholder="如:清理日志" allow-clear />
|
||||
<a-input
|
||||
v-model:value="form.name"
|
||||
placeholder="如:清理日志"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 任务类型 -->
|
||||
<a-form-item label="任务类型" name="type" required>
|
||||
<a-select v-model:value="form.type" placeholder="请选择任务类型">
|
||||
<a-select
|
||||
v-model:value="form.type"
|
||||
placeholder="请选择任务类型"
|
||||
>
|
||||
<a-select-option value="command">命令</a-select-option>
|
||||
<a-select-option value="job">任务</a-select-option>
|
||||
<a-select-option value="closure">闭包</a-select-option>
|
||||
@@ -17,38 +37,74 @@
|
||||
|
||||
<!-- 命令/类名 -->
|
||||
<a-form-item label="命令/类" name="command" required>
|
||||
<a-textarea v-if="form.type === 'command'" v-model:value="form.command" placeholder="如:php artisan schedule:run"
|
||||
:rows="3" />
|
||||
<a-input v-else v-model:value="form.command" placeholder="如:App\Jobs\SendEmailJob" allow-clear />
|
||||
<a-textarea
|
||||
v-if="form.type === 'command'"
|
||||
v-model:value="form.command"
|
||||
placeholder="如:php artisan schedule:run"
|
||||
:rows="3"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
v-model:value="form.command"
|
||||
placeholder="如:App\Jobs\SendEmailJob"
|
||||
allow-clear
|
||||
/>
|
||||
<div class="form-tip">
|
||||
<span v-if="form.type === 'command'">Shell命令或Artisan命令</span>
|
||||
<span v-else-if="form.type === 'job'">任务类的完整命名空间</span>
|
||||
<span v-if="form.type === 'command'"
|
||||
>Shell命令或Artisan命令</span
|
||||
>
|
||||
<span v-else-if="form.type === 'job'"
|
||||
>任务类的完整命名空间</span
|
||||
>
|
||||
<span v-else>闭包函数的代码</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- Cron表达式 -->
|
||||
<a-form-item label="Cron表达式" name="expression" required>
|
||||
<a-input v-model:value="form.expression" placeholder="* * * * *" allow-clear />
|
||||
<a-input
|
||||
v-model:value="form.expression"
|
||||
placeholder="* * * * *"
|
||||
allow-clear
|
||||
/>
|
||||
<div class="form-tip">
|
||||
分 时 日 月 周,如:0 0 * * * (每天0点执行)
|
||||
<a-link href="https://crontab.guru/" target="_blank" style="color: #1890ff">在线生成工具</a-link>
|
||||
<a-link
|
||||
href="https://crontab.guru/"
|
||||
target="_blank"
|
||||
style="color: #1890ff"
|
||||
>在线生成工具</a-link
|
||||
>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 时区 -->
|
||||
<a-form-item label="时区" name="timezone">
|
||||
<a-select v-model:value="form.timezone" placeholder="请选择时区" show-search :filter-option="filterOption">
|
||||
<a-select-option value="Asia/Shanghai">Asia/Shanghai (中国)</a-select-option>
|
||||
<a-select
|
||||
v-model:value="form.timezone"
|
||||
placeholder="请选择时区"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option value="Asia/Shanghai"
|
||||
>Asia/Shanghai (中国)</a-select-option
|
||||
>
|
||||
<a-select-option value="UTC">UTC</a-select-option>
|
||||
<a-select-option value="America/New_York">America/New_York</a-select-option>
|
||||
<a-select-option value="America/New_York"
|
||||
>America/New_York</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 描述 -->
|
||||
<a-form-item label="任务描述" name="description">
|
||||
<a-textarea v-model:value="form.description" placeholder="请输入任务描述" :rows="3" maxlength="200"
|
||||
show-count />
|
||||
<a-textarea
|
||||
v-model:value="form.description"
|
||||
placeholder="请输入任务描述"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
@@ -71,291 +127,326 @@
|
||||
|
||||
<!-- 启用状态 -->
|
||||
<a-form-item label="启用状态" name="is_active">
|
||||
<sc-select v-model:value="form.is_active" source-type="dictionary" dictionary-code="yes_no" placeholder="请选择状态" allow-clear />
|
||||
<sc-select
|
||||
v-model:value="form.is_active"
|
||||
source-type="dictionary"
|
||||
dictionary-code="yes_no"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 排序 -->
|
||||
<a-form-item label="排序" name="sort">
|
||||
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
|
||||
<a-input-number
|
||||
v-model:value="form.sort"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
<!-- 底部按钮 -->
|
||||
<div class="dialog-footer">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="isSaving" @click="handleSubmit">保存</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="isSaving"
|
||||
@click="handleSubmit"
|
||||
>保存</a-button
|
||||
>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import scSelect from '@/components/scSelect/index.vue'
|
||||
import systemApi from '@/api/system'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import scSelect from "@/components/scSelect/index.vue";
|
||||
import systemApi from "@/api/system";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref(null)
|
||||
const isSaving = ref(false)
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
const formRef = ref(null);
|
||||
const isSaving = ref(false);
|
||||
const isEdit = computed(() => !!props.record?.id);
|
||||
|
||||
const title = computed(() => {
|
||||
return isEdit.value ? '编辑定时任务' : '新增定时任务'
|
||||
})
|
||||
return isEdit.value ? "编辑定时任务" : "新增定时任务";
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
id: '',
|
||||
name: '',
|
||||
command: '',
|
||||
type: 'command',
|
||||
expression: '* * * * *',
|
||||
timezone: 'Asia/Shanghai',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
command: "",
|
||||
type: "command",
|
||||
expression: "* * * * *",
|
||||
timezone: "Asia/Shanghai",
|
||||
description: "",
|
||||
is_active: null,
|
||||
run_in_background: false,
|
||||
without_overlapping: false,
|
||||
only_one: false,
|
||||
sort: 0
|
||||
})
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
// Cron 表达式验证函数
|
||||
const validateCronExpression = (rule, value) => {
|
||||
if (!value || !value.trim()) {
|
||||
return Promise.reject('请输入Cron表达式')
|
||||
return Promise.reject("请输入Cron表达式");
|
||||
}
|
||||
|
||||
const parts = value.trim().split(/\s+/)
|
||||
const parts = value.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
return Promise.reject('Cron表达式应由5部分组成:分 时 日 月 周')
|
||||
return Promise.reject("Cron表达式应由5部分组成:分 时 日 月 周");
|
||||
}
|
||||
|
||||
const [minute, hour, day, month, weekday] = parts
|
||||
const [minute, hour, day, month, weekday] = parts;
|
||||
|
||||
// 验证分钟 (0-59)
|
||||
if (!validateCronPart(minute, 0, 59)) {
|
||||
return Promise.reject('分钟部分格式不正确 (0-59)')
|
||||
return Promise.reject("分钟部分格式不正确 (0-59)");
|
||||
}
|
||||
|
||||
// 验证小时 (0-23)
|
||||
if (!validateCronPart(hour, 0, 23)) {
|
||||
return Promise.reject('小时部分格式不正确 (0-23)')
|
||||
return Promise.reject("小时部分格式不正确 (0-23)");
|
||||
}
|
||||
|
||||
// 验证日 (1-31)
|
||||
if (!validateCronPart(day, 1, 31)) {
|
||||
return Promise.reject('日部分格式不正确 (1-31)')
|
||||
return Promise.reject("日部分格式不正确 (1-31)");
|
||||
}
|
||||
|
||||
// 验证月 (1-12)
|
||||
if (!validateCronPart(month, 1, 12)) {
|
||||
return Promise.reject('月部分格式不正确 (1-12)')
|
||||
return Promise.reject("月部分格式不正确 (1-12)");
|
||||
}
|
||||
|
||||
// 验证周 (0-6 或 SUN-SAT)
|
||||
if (!validateCronPart(weekday, 0, 6, true)) {
|
||||
return Promise.reject('周部分格式不正确 (0-6 或 SUN-SAT)')
|
||||
return Promise.reject("周部分格式不正确 (0-6 或 SUN-SAT)");
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// 验证 Cron 单个部分
|
||||
const validateCronPart = (part, min, max, allowDayNames = false) => {
|
||||
// 支持 * (所有值)
|
||||
if (part === '*') return true
|
||||
if (part === "*") return true;
|
||||
|
||||
// 支持逗号分隔的列表
|
||||
const listItems = part.split(',')
|
||||
const listItems = part.split(",");
|
||||
for (const item of listItems) {
|
||||
// 支持步长 (step),如 */5 或 0-10/2
|
||||
if (item.includes('/')) {
|
||||
const [range, step] = item.split('/')
|
||||
if (!range || !step) return false
|
||||
if (!validateCronRange(range, min, max, allowDayNames)) return false
|
||||
if (!/^\d+$/.test(step)) return false
|
||||
continue
|
||||
if (item.includes("/")) {
|
||||
const [range, step] = item.split("/");
|
||||
if (!range || !step) return false;
|
||||
if (!validateCronRange(range, min, max, allowDayNames))
|
||||
return false;
|
||||
if (!/^\d+$/.test(step)) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 支持范围,如 1-5
|
||||
if (item.includes('-')) {
|
||||
if (!validateCronRange(item, min, max, allowDayNames)) return false
|
||||
continue
|
||||
if (item.includes("-")) {
|
||||
if (!validateCronRange(item, min, max, allowDayNames)) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 支持星期名称
|
||||
if (allowDayNames) {
|
||||
const dayNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
|
||||
const dayNamesLower = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
const dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||
const dayNamesLower = [
|
||||
"sun",
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
];
|
||||
if (dayNames.includes(item) || dayNamesLower.includes(item)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为有效数字
|
||||
if (!/^\d+$/.test(item)) return false
|
||||
const num = parseInt(item, 10)
|
||||
if (num < min || num > max) return false
|
||||
if (!/^\d+$/.test(item)) return false;
|
||||
const num = parseInt(item, 10);
|
||||
if (num < min || num > max) return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 验证 Cron 范围
|
||||
const validateCronRange = (range, min, max, allowDayNames = false) => {
|
||||
// 处理 * 范围
|
||||
if (range === '*') return true
|
||||
if (range === "*") return true;
|
||||
|
||||
const parts = range.split('-')
|
||||
if (parts.length !== 2) return false
|
||||
const parts = range.split("-");
|
||||
if (parts.length !== 2) return false;
|
||||
|
||||
const [start, end] = parts
|
||||
const [start, end] = parts;
|
||||
|
||||
// 支持星期名称范围
|
||||
if (allowDayNames) {
|
||||
const dayNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
|
||||
const startIdx = dayNames.indexOf(start)
|
||||
const endIdx = dayNames.indexOf(end)
|
||||
const dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||
const startIdx = dayNames.indexOf(start);
|
||||
const endIdx = dayNames.indexOf(end);
|
||||
if (startIdx !== -1 && endIdx !== -1) {
|
||||
return startIdx <= endIdx
|
||||
return startIdx <= endIdx;
|
||||
}
|
||||
}
|
||||
|
||||
// 数字范围验证
|
||||
if (!/^\d+$/.test(start) || !/^\d+$/.test(end)) return false
|
||||
const startNum = parseInt(start, 10)
|
||||
const endNum = parseInt(end, 10)
|
||||
if (!/^\d+$/.test(start) || !/^\d+$/.test(end)) return false;
|
||||
const startNum = parseInt(start, 10);
|
||||
const endNum = parseInt(end, 10);
|
||||
|
||||
return startNum >= min && startNum <= max && endNum >= min && endNum <= max && startNum <= endNum
|
||||
}
|
||||
return (
|
||||
startNum >= min &&
|
||||
startNum <= max &&
|
||||
endNum >= min &&
|
||||
endNum <= max &&
|
||||
startNum <= endNum
|
||||
);
|
||||
};
|
||||
|
||||
// 验证规则
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入任务名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择任务类型', trigger: 'change' }
|
||||
],
|
||||
command: [
|
||||
{ required: true, message: '请输入命令或类名', trigger: 'blur' }
|
||||
{ required: true, message: "请输入任务名称", trigger: "blur" },
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: "长度在 2 到 100 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
type: [{ required: true, message: "请选择任务类型", trigger: "change" }],
|
||||
command: [{ required: true, message: "请输入命令或类名", trigger: "blur" }],
|
||||
expression: [
|
||||
{ required: true, message: '请输入Cron表达式', trigger: 'blur' },
|
||||
{ validator: validateCronExpression, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
{ required: true, message: "请输入Cron表达式", trigger: "blur" },
|
||||
{ validator: validateCronExpression, trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 下拉筛选
|
||||
const filterOption = (input, option) => {
|
||||
return option.value.toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
return option.value.toLowerCase().includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
id: '',
|
||||
name: '',
|
||||
command: '',
|
||||
type: 'command',
|
||||
expression: '* * * * *',
|
||||
timezone: 'Asia/Shanghai',
|
||||
description: '',
|
||||
id: "",
|
||||
name: "",
|
||||
command: "",
|
||||
type: "command",
|
||||
expression: "* * * * *",
|
||||
timezone: "Asia/Shanghai",
|
||||
description: "",
|
||||
is_active: null,
|
||||
run_in_background: false,
|
||||
without_overlapping: false,
|
||||
only_one: false,
|
||||
sort: 0
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
sort: 0,
|
||||
};
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 设置数据
|
||||
const setData = (data) => {
|
||||
if (data) {
|
||||
form.value = {
|
||||
id: data.id || '',
|
||||
name: data.name || '',
|
||||
command: data.command || '',
|
||||
type: data.type || 'command',
|
||||
expression: data.expression || '* * * * *',
|
||||
timezone: data.timezone || 'Asia/Shanghai',
|
||||
description: data.description || '',
|
||||
id: data.id || "",
|
||||
name: data.name || "",
|
||||
command: data.command || "",
|
||||
type: data.type || "command",
|
||||
expression: data.expression || "* * * * *",
|
||||
timezone: data.timezone || "Asia/Shanghai",
|
||||
description: data.description || "",
|
||||
is_active: data.is_active !== undefined ? data.is_active : null,
|
||||
run_in_background: data.run_in_background || false,
|
||||
without_overlapping: data.without_overlapping || false,
|
||||
only_one: data.only_one || false,
|
||||
sort: data.sort !== undefined ? data.sort : 0
|
||||
}
|
||||
sort: data.sort !== undefined ? data.sort : 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
await formRef.value.validate();
|
||||
|
||||
isSaving.value = true
|
||||
isSaving.value = true;
|
||||
|
||||
const submitData = { ...form.value }
|
||||
const submitData = { ...form.value };
|
||||
|
||||
let res = {}
|
||||
let res = {};
|
||||
if (isEdit.value) {
|
||||
res = await systemApi.tasks.edit.put(form.value.id, submitData)
|
||||
res = await systemApi.tasks.edit.put(form.value.id, submitData);
|
||||
} else {
|
||||
res = await systemApi.tasks.add.post(submitData)
|
||||
res = await systemApi.tasks.add.post(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
message.success(isEdit.value ? "编辑成功" : "新增成功");
|
||||
emit("success");
|
||||
handleCancel();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.errorFields) {
|
||||
console.log('表单验证失败:', error)
|
||||
console.log("表单验证失败:", error);
|
||||
} else {
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败')
|
||||
console.error("提交失败:", error);
|
||||
message.error("操作失败");
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
resetForm();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
if (props.record) {
|
||||
setData(props.record)
|
||||
} else {
|
||||
resetForm()
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.record) {
|
||||
setData(props.record);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,13 +3,28 @@
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="任务名称/命令" allow-clear style="width: 180px" />
|
||||
<a-select v-model:value="searchForm.type" placeholder="任务类型" allow-clear style="width: 120px">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="任务名称/命令"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="任务类型"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="command">命令</a-select-option>
|
||||
<a-select-option value="job">任务</a-select-option>
|
||||
<a-select-option value="closure">闭包</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="searchForm.is_active" placeholder="状态" allow-clear style="width: 100px">
|
||||
<a-select
|
||||
v-model:value="searchForm.is_active"
|
||||
placeholder="状态"
|
||||
allow-clear
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option :value="true">启用</a-select-option>
|
||||
<a-select-option :value="false">禁用</a-select-option>
|
||||
</a-select>
|
||||
@@ -52,15 +67,29 @@
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading"
|
||||
:pagination="pagination" :row-selection="rowSelection" :row-key="rowKey" @refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange">
|
||||
<scTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
:row-key="rowKey"
|
||||
@refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange"
|
||||
>
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getTypeColor(record.type)">{{ getTypeText(record.type) }}</a-tag>
|
||||
<a-tag :color="getTypeColor(record.type)">{{
|
||||
getTypeText(record.type)
|
||||
}}</a-tag>
|
||||
</template>
|
||||
|
||||
<template #is_active="{ record }">
|
||||
<a-switch :checked="record.is_active" :disabled="!canEdit" @change="handleToggleStatus(record)" />
|
||||
<a-switch
|
||||
:checked="record.is_active"
|
||||
:disabled="!canEdit"
|
||||
@change="handleToggleStatus(record)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #expression="{ record }">
|
||||
@@ -68,7 +97,11 @@
|
||||
</template>
|
||||
|
||||
<template #last_run_at="{ record }">
|
||||
{{ record.last_run_at ? formatDate(record.last_run_at) : '-' }}
|
||||
{{
|
||||
record.last_run_at
|
||||
? formatDate(record.last_run_at)
|
||||
: "-"
|
||||
}}
|
||||
</template>
|
||||
|
||||
<template #next_run_at="{ record }">
|
||||
@@ -83,12 +116,20 @@
|
||||
<a-space>
|
||||
<a-tooltip title="成功次数">
|
||||
<a-tag color="success">
|
||||
<CheckCircleOutlined /> {{ record.run_count || 0 }}
|
||||
<CheckCircleOutlined />
|
||||
{{ record.run_count || 0 }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="失败次数">
|
||||
<a-tag :color="record.failed_count > 0 ? 'error' : 'default'">
|
||||
<CloseCircleOutlined /> {{ record.failed_count || 0 }}
|
||||
<a-tag
|
||||
:color="
|
||||
record.failed_count > 0
|
||||
? 'error'
|
||||
: 'default'
|
||||
"
|
||||
>
|
||||
<CloseCircleOutlined />
|
||||
{{ record.failed_count || 0 }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
@@ -96,18 +137,32 @@
|
||||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleView(record)"
|
||||
>
|
||||
<EyeOutlined />查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定立即执行该任务吗?" @confirm="handleRun(record)">
|
||||
<a-popconfirm
|
||||
title="确定立即执行该任务吗?"
|
||||
@confirm="handleRun(record)"
|
||||
>
|
||||
<a-button type="link" size="small">
|
||||
<PlayCircleOutlined />执行
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确定删除该任务吗?" @confirm="handleDelete(record)">
|
||||
<a-popconfirm
|
||||
title="确定删除该任务吗?"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
@@ -119,15 +174,24 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<TaskDialog v-if="dialog.save" v-model:visible="dialog.save" :record="currentRecord" @success="handleSaveSuccess" />
|
||||
<TaskDialog
|
||||
v-if="dialog.save"
|
||||
v-model:visible="dialog.save"
|
||||
:record="currentRecord"
|
||||
@success="handleSaveSuccess"
|
||||
/>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<TaskDetailDialog v-if="dialog.detail" v-model:visible="dialog.detail" :record="currentRecord" />
|
||||
<TaskDetailDialog
|
||||
v-if="dialog.detail"
|
||||
v-model:visible="dialog.detail"
|
||||
:record="currentRecord"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ref, reactive } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
RedoOutlined,
|
||||
@@ -140,13 +204,13 @@ import {
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import systemApi from '@/api/system'
|
||||
import scTable from '@/components/scTable/index.vue'
|
||||
import TaskDialog from './components/TaskDialog.vue'
|
||||
import TaskDetailDialog from './components/TaskDetailDialog.vue'
|
||||
CloseCircleOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useTable } from "@/hooks/useTable";
|
||||
import systemApi from "@/api/system";
|
||||
import scTable from "@/components/scTable/index.vue";
|
||||
import TaskDialog from "./components/TaskDialog.vue";
|
||||
import TaskDetailDialog from "./components/TaskDetailDialog.vue";
|
||||
|
||||
// ===== useTable Hook =====
|
||||
const {
|
||||
@@ -160,202 +224,261 @@ const {
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePaginationChange,
|
||||
refreshTable
|
||||
refreshTable,
|
||||
} = useTable({
|
||||
api: systemApi.tasks.list.get,
|
||||
searchForm: {
|
||||
keyword: '',
|
||||
keyword: "",
|
||||
type: undefined,
|
||||
is_active: undefined
|
||||
is_active: undefined,
|
||||
},
|
||||
columns: [],
|
||||
needPagination: true,
|
||||
needSelection: true
|
||||
})
|
||||
needSelection: true,
|
||||
});
|
||||
|
||||
// ===== 表格列配置 =====
|
||||
const rowKey = 'id'
|
||||
const rowKey = "id";
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
|
||||
{ title: '任务名称', dataIndex: 'name', key: 'name', width: 150, ellipsis: true },
|
||||
{ title: '任务类型', dataIndex: 'type', key: 'type', width: 100, align: 'center', slot: 'type' },
|
||||
{ title: '命令/类', dataIndex: 'command', key: 'command', ellipsis: true },
|
||||
{ title: 'Cron表达式', dataIndex: 'expression', key: 'expression', width: 120, align: 'center', slot: 'expression' },
|
||||
{ title: '状态', dataIndex: 'is_active', key: 'is_active', width: 80, align: 'center', slot: 'is_active' },
|
||||
{ title: '上次运行', dataIndex: 'last_run_at', key: 'last_run_at', width: 160, align: 'center', slot: 'last_run_at' },
|
||||
{ title: '下次运行', dataIndex: 'next_run_at', key: 'next_run_at', width: 160, align: 'center', slot: 'next_run_at' },
|
||||
{ title: '统计', dataIndex: 'statistics', key: 'statistics', width: 120, align: 'center', slot: 'statistics' },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 200, align: 'center', fixed: 'right', slot: 'action' }
|
||||
]
|
||||
{ title: "ID", dataIndex: "id", key: "id", width: 80, align: "center" },
|
||||
{
|
||||
title: "任务名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "任务类型",
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: 100,
|
||||
align: "center",
|
||||
slot: "type",
|
||||
},
|
||||
{ title: "命令/类", dataIndex: "command", key: "command", ellipsis: true },
|
||||
{
|
||||
title: "Cron表达式",
|
||||
dataIndex: "expression",
|
||||
key: "expression",
|
||||
width: 120,
|
||||
align: "center",
|
||||
slot: "expression",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "is_active",
|
||||
key: "is_active",
|
||||
width: 80,
|
||||
align: "center",
|
||||
slot: "is_active",
|
||||
},
|
||||
{
|
||||
title: "上次运行",
|
||||
dataIndex: "last_run_at",
|
||||
key: "last_run_at",
|
||||
width: 160,
|
||||
align: "center",
|
||||
slot: "last_run_at",
|
||||
},
|
||||
{
|
||||
title: "下次运行",
|
||||
dataIndex: "next_run_at",
|
||||
key: "next_run_at",
|
||||
width: 160,
|
||||
align: "center",
|
||||
slot: "next_run_at",
|
||||
},
|
||||
{
|
||||
title: "统计",
|
||||
dataIndex: "statistics",
|
||||
key: "statistics",
|
||||
width: 120,
|
||||
align: "center",
|
||||
slot: "statistics",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
dataIndex: "action",
|
||||
key: "action",
|
||||
width: 200,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
slot: "action",
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 弹窗状态 =====
|
||||
const dialog = reactive({
|
||||
save: false,
|
||||
detail: false
|
||||
})
|
||||
detail: false,
|
||||
});
|
||||
|
||||
const currentRecord = ref(null)
|
||||
const currentRecord = ref(null);
|
||||
|
||||
// ===== 权限控制 =====
|
||||
const canEdit = ref(true)
|
||||
const canEdit = ref(true);
|
||||
|
||||
// ===== 方法:获取任务类型文本 =====
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
command: '命令',
|
||||
job: '任务',
|
||||
closure: '闭包'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
command: "命令",
|
||||
job: "任务",
|
||||
closure: "闭包",
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
// ===== 方法:获取任务类型颜色 =====
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
command: 'blue',
|
||||
job: 'green',
|
||||
closure: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
command: "blue",
|
||||
job: "green",
|
||||
closure: "orange",
|
||||
};
|
||||
return colorMap[type] || "default";
|
||||
};
|
||||
|
||||
// ===== 方法:格式化日期 =====
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
if (!dateStr) return "-";
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString("zh-CN");
|
||||
};
|
||||
|
||||
// ===== 方法:切换状态 =====
|
||||
const handleToggleStatus = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.batchStatus.post({
|
||||
ids: [record.id],
|
||||
status: record.is_active
|
||||
})
|
||||
status: record.is_active,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success(record.is_active ? '已启用' : '已禁用')
|
||||
refreshTable()
|
||||
message.success(record.is_active ? "已启用" : "已禁用");
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
message.error("操作失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:新增 =====
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
dialog.save = true
|
||||
}
|
||||
currentRecord.value = null;
|
||||
dialog.save = true;
|
||||
};
|
||||
|
||||
// ===== 方法:编辑 =====
|
||||
const handleEdit = (record) => {
|
||||
currentRecord.value = { ...record }
|
||||
dialog.save = true
|
||||
}
|
||||
currentRecord.value = { ...record };
|
||||
dialog.save = true;
|
||||
};
|
||||
|
||||
// ===== 方法:查看 =====
|
||||
const handleView = (record) => {
|
||||
currentRecord.value = { ...record }
|
||||
dialog.detail = true
|
||||
}
|
||||
currentRecord.value = { ...record };
|
||||
dialog.detail = true;
|
||||
};
|
||||
|
||||
// ===== 方法:删除 =====
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.delete.delete(record.id)
|
||||
const res = await systemApi.tasks.delete.delete(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
refreshTable()
|
||||
message.success("删除成功");
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
message.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
message.error("删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:执行任务 =====
|
||||
const handleRun = async (record) => {
|
||||
try {
|
||||
const res = await systemApi.tasks.run.post(record.id)
|
||||
const res = await systemApi.tasks.run.post(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('任务执行成功')
|
||||
refreshTable()
|
||||
message.success("任务执行成功");
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '任务执行失败')
|
||||
message.error(res.message || "任务执行失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('任务执行失败')
|
||||
message.error("任务执行失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 方法:批量删除 =====
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要删除的任务')
|
||||
return
|
||||
message.warning("请选择要删除的任务");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
title: "确认删除",
|
||||
content: `确定删除选中的 ${selectedRows.value.length} 个任务吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const res = await systemApi.tasks.batchDelete.post({ ids })
|
||||
const ids = selectedRows.value.map((item) => item.id);
|
||||
const res = await systemApi.tasks.batchDelete.post({ ids });
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功')
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
message.success("删除成功");
|
||||
selectedRows.value = [];
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
message.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
message.error("删除失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 方法:批量更新状态 =====
|
||||
const handleBatchStatus = (status) => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
message.warning('请选择要操作的任务')
|
||||
return
|
||||
message.warning("请选择要操作的任务");
|
||||
return;
|
||||
}
|
||||
|
||||
const statusText = status ? '启用' : '禁用'
|
||||
const statusText = status ? "启用" : "禁用";
|
||||
|
||||
Modal.confirm({
|
||||
title: `确认${statusText}`,
|
||||
content: `确定要${statusText}选中的 ${selectedRows.value.length} 个任务吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const ids = selectedRows.value.map(item => item.id)
|
||||
const res = await systemApi.tasks.batchStatus.post({ ids, status })
|
||||
const ids = selectedRows.value.map((item) => item.id);
|
||||
const res = await systemApi.tasks.batchStatus.post({
|
||||
ids,
|
||||
status,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success(`${statusText}成功`)
|
||||
selectedRows.value = []
|
||||
refreshTable()
|
||||
message.success(`${statusText}成功`);
|
||||
selectedRows.value = [];
|
||||
refreshTable();
|
||||
} else {
|
||||
message.error(res.message || '操作失败')
|
||||
message.error(res.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
message.error("操作失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 方法:保存成功回调 =====
|
||||
const handleSaveSuccess = () => {
|
||||
dialog.save = false
|
||||
refreshTable()
|
||||
}
|
||||
dialog.save = false;
|
||||
refreshTable();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -364,7 +487,7 @@ const handleSaveSuccess = () => {
|
||||
padding: 2px 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-family: "Consolas", "Monaco", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user