This commit is contained in:
2026-01-20 22:10:55 +08:00
parent 8e55f7de9d
commit 656c328aef
8 changed files with 1707 additions and 323 deletions

View File

@@ -1,13 +1,341 @@
import request from '../utils/request'
import request from "@/utils/request";
/**
* 用户登录
* @returns {Promise} 菜单数据
*/
export function upload(params) {
return request({
url: '/system/file/upload',
method: 'post',
data: params
})
}
export default {
version: {
url: `system/index/version`,
name: "获取最新版本号",
get: async function () {
return await request.get(this.url);
},
},
clearcache: {
url: `system/index/clearcache`,
name: "清除缓存",
post: async function () {
return await request.post(this.url);
},
},
info: {
url: `system/index/info`,
name: "系统信息",
get: function (data) {
return request.get(this.url, data);
},
},
setting: {
list: {
url: `system/setting/index`,
name: "获取配置信息",
get: function (params) {
return request.get(this.url, params);
},
},
fields: {
url: `system/setting/fields`,
name: "获取配置字段",
get: async function (params) {
return await request.get(this.url, params);
},
},
add: {
url: `system/setting/add`,
name: "保存配置信息",
post: function (data) {
return request.post(this.url, data);
},
},
edit: {
url: `system/setting/edit`,
name: "编辑配置信息",
post: function (data) {
return request.put(this.url, data);
},
},
save: {
url: `system/setting/save`,
name: "保存配置信息",
post: function (data) {
return request.put(this.url, data);
},
},
},
dictionary: {
category: {
url: `system/dict/category`,
name: "获取字典树",
get: async function (params) {
return await request.get(this.url, params);
},
},
editcate: {
url: `system/dict/editcate`,
name: "编辑字典树",
post: async function (data = {}) {
return await request.put(this.url, data);
},
},
addcate: {
url: `system/dict/addcate`,
name: "添加字典树",
post: async function (data = {}) {
return await request.post(this.url, data);
},
},
delCate: {
url: `system/dict/deletecate`,
name: "删除字典树",
post: async function (data = {}) {
return await request.delete(this.url, data);
},
},
list: {
url: `system/dict/lists`,
name: "字典明细",
get: async function (params) {
return await request.get(this.url, params);
},
},
get: {
url: `system/dict/detail`,
name: "获取字典数据",
get: async function (params) {
return await request.get(this.url, params);
},
},
edit: {
url: `system/dict/edit`,
name: "编辑字典明细",
post: async function (data = {}) {
return await request.put(this.url, data);
},
},
add: {
url: `system/dict/add`,
name: "添加字典明细",
post: async function (data = {}) {
return await request.post(this.url, data);
},
},
delete: {
url: `system/dict/delete`,
name: "删除字典明细",
post: async function (data = {}) {
return await request.delete(this.url, data);
},
},
detail: {
url: `system/dict/detail`,
name: "字典明细",
get: async function (params) {
return await request.get(this.url, params);
},
},
alldic: {
url: `system/dict/all`,
name: "全部字典",
get: async function (params) {
return await request.get(this.url, params);
},
},
},
area: {
list: {
url: `system/area/index`,
name: "地区列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
add: {
url: `system/area/add`,
name: "地区添加",
post: async function (params) {
return await request.post(this.url, params);
},
},
edit: {
url: `system/area/edit`,
name: "地区编辑",
post: async function (params) {
return await request.put(this.url, params);
},
},
},
app: {
list: {
url: `system/app/list`,
name: "应用列表",
get: async function () {
return await request.get(this.url);
},
},
},
client: {
list: {
url: `system/client/index`,
name: "客户端列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
add: {
url: `system/client/add`,
name: "客户端添加",
post: async function (params) {
return await request.post(this.url, params);
},
},
edit: {
url: `system/client/edit`,
name: "客户端编辑",
post: async function (params) {
return await request.put(this.url, params);
},
},
delete: {
url: `system/client/delete`,
name: "客户端删除",
post: async function (params) {
return await request.delete(this.url, params);
},
},
menu: {
list: {
url: `system/menu/index`,
name: "客户端菜单列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
add: {
url: `system/menu/add`,
name: "客户端菜单添加",
post: async function (params) {
return await request.post(this.url, params);
},
},
edit: {
url: `system/menu/edit`,
name: "客户端菜单编辑",
post: async function (params) {
return await request.put(this.url, params);
},
},
delete: {
url: `system/menu/delete`,
name: "客户端菜单删除",
post: async function (params) {
return await request.delete(this.url, params);
},
},
},
},
log: {
list: {
url: `system/log/index`,
name: "日志列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
my: {
url: `system/log/my`,
name: "我的日志",
get: async function (params) {
return await request.get(this.url, params);
},
},
delete: {
url: `system/log/delete`,
name: "日志删除",
post: async function (params) {
return await request.delete(this.url, params);
},
},
},
tasks: {
list: {
url: `system/tasks/index`,
name: "任务列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
delete: {
url: `system/tasks/delete`,
name: "任务删除",
post: async function (params) {
return await request.delete(this.url, params);
},
},
},
crontab: {
list: {
url: `system/crontab/index`,
name: "定时任务列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
add: {
url: `system/crontab/add`,
name: "定时任务添加",
post: async function (params) {
return await request.post(this.url, params);
},
},
edit: {
url: `system/crontab/edit`,
name: "定时任务编辑",
post: async function (params) {
return await request.put(this.url, params);
},
},
delete: {
url: `system/crontab/delete`,
name: "定时任务删除",
post: async function (params) {
return await request.delete(this.url, params);
},
},
log: {
url: `system/crontab/log`,
name: "定时任务日志",
get: async function (params) {
return await request.get(this.url, params);
},
},
reload: {
url: `system/crontab/reload`,
name: "定时任务重载",
post: async function (params) {
return await request.put(this.url, params);
},
},
},
modules: {
list: {
url: `system/modules/index`,
name: "模块列表",
get: async function (params) {
return await request.get(this.url, params);
},
},
update: {
url: `system/modules/update`,
name: "更新模块",
post: async function (params) {
return await request.post(this.url, params);
},
},
},
sms: {
count: {
url: `system/sms/count`,
name: "短信发送统计",
get: async function (params) {
return await request.get(this.url, params);
},
},
},
};

View File

@@ -5,6 +5,37 @@ export default {
logout: 'Logout',
register: 'Register',
searchMenu: 'Search Menu',
searchPlaceholder: 'Please enter menu name to search',
noResults: 'No matching menus found',
searchTips: 'Keyboard Shortcuts Tips',
navigateResults: 'Use up/down arrows to navigate',
selectResult: 'Press Enter to select',
closeSearch: 'Press ESC to close',
taskCenter: 'Task Center',
totalTasks: 'Total Tasks',
pendingTasks: 'Pending',
completedTasks: 'Completed',
searchTasks: 'Search tasks...',
all: 'All',
pending: 'Pending',
completed: 'Completed',
taskTitle: 'Task Title',
enterTaskTitle: 'Please enter task title',
taskPriority: 'Task Priority',
priorityHigh: 'High',
priorityMedium: 'Medium',
priorityLow: 'Low',
confirmDelete: 'Confirm Delete',
addTask: 'Add Task',
pleaseEnterTaskTitle: 'Please enter task title',
added: 'Added',
deleted: 'Deleted',
justNow: 'Just now',
clearCache: 'Clear Cache',
confirmClearCache: 'Confirm Clear Cache',
clearCacheConfirm: 'Are you sure you want to clear all cache? This will clear local storage, session storage and cached data.',
cacheCleared: 'Cache cleared',
clearCacheFailed: 'Failed to clear cache',
messages: 'Messages',
tasks: 'Tasks',
clearAll: 'Clear All',
@@ -62,6 +93,33 @@ export default {
info: 'Info',
confirmDelete: 'Are you sure you want to delete?',
confirmLogout: 'Are you sure you want to logout?',
addConfig: 'Add Config',
editConfig: 'Edit Config',
configCategory: 'Config Category',
configName: 'Config Name',
configTitle: 'Config Title',
configType: 'Config Type',
configValue: 'Config Value',
configTip: 'Config Tip',
typeText: 'Text',
typeTextarea: 'Textarea',
typeNumber: 'Number',
typeSwitch: 'Switch',
typeSelect: 'Select',
typeMultiselect: 'Multiselect',
typeDatetime: 'Datetime',
typeColor: 'Color',
pleaseSelect: 'Please Select',
pleaseEnter: 'Please Enter',
noConfig: 'No Config',
fetchConfigFailed: 'Failed to fetch config',
addSuccess: 'Added Successfully',
addFailed: 'Failed to Add',
editSuccess: 'Edited Successfully',
editFailed: 'Failed to Edit',
saveSuccess: 'Saved Successfully',
saveFailed: 'Failed to Save',
resetSuccess: 'Reset Successfully',
required: 'This field is required',
operation: 'Operation',
time: 'Time',

View File

@@ -5,6 +5,37 @@ export default {
logout: '退出登录',
register: '注册',
searchMenu: '搜索菜单',
searchPlaceholder: '请输入菜单名称进行搜索',
noResults: '未找到匹配的菜单',
searchTips: '快捷键操作提示',
navigateResults: '使用上下键导航',
selectResult: '按回车键选择',
closeSearch: '按 ESC 关闭',
taskCenter: '任务中心',
totalTasks: '总任务',
pendingTasks: '待完成',
completedTasks: '已完成',
searchTasks: '搜索任务...',
all: '全部',
pending: '待完成',
completed: '已完成',
taskTitle: '任务标题',
enterTaskTitle: '请输入任务标题',
taskPriority: '任务优先级',
priorityHigh: '高',
priorityMedium: '中',
priorityLow: '低',
confirmDelete: '确认删除',
addTask: '添加任务',
pleaseEnterTaskTitle: '请输入任务标题',
added: '已添加',
deleted: '已删除',
justNow: '刚刚',
clearCache: '清除缓存',
confirmClearCache: '确认清除缓存',
clearCacheConfirm: '确定要清除所有缓存吗?这将清除本地存储、会话存储和缓存数据。',
cacheCleared: '缓存已清除',
clearCacheFailed: '清除缓存失败',
messages: '消息',
tasks: '任务',
clearAll: '清空全部',
@@ -62,6 +93,33 @@ export default {
info: '提示',
confirmDelete: '确定要删除吗?',
confirmLogout: '确定要退出登录吗?',
addConfig: '添加配置',
editConfig: '编辑配置',
configCategory: '配置分类',
configName: '配置名称',
configTitle: '配置标题',
configType: '配置类型',
configValue: '配置值',
configTip: '配置提示',
typeText: '文本',
typeTextarea: '文本域',
typeNumber: '数字',
typeSwitch: '开关',
typeSelect: '下拉选择',
typeMultiselect: '多选',
typeDatetime: '日期时间',
typeColor: '颜色',
pleaseSelect: '请选择',
pleaseEnter: '请输入',
noConfig: '暂无配置',
fetchConfigFailed: '获取配置失败',
addSuccess: '添加成功',
addFailed: '添加失败',
editSuccess: '编辑成功',
editFailed: '编辑失败',
saveSuccess: '保存成功',
saveFailed: '保存失败',
resetSuccess: '重置成功',
required: '此项为必填项',
operation: '操作',
time: '时间',

View File

@@ -0,0 +1,302 @@
<template>
<a-modal
v-model:open="visible"
:title="$t('common.searchMenu')"
:footer="null"
:width="600"
:destroyOnClose="true"
@cancel="handleClose"
>
<div class="menu-search">
<a-input
v-model:value="searchKeyword"
:placeholder="$t('common.searchPlaceholder')"
size="large"
allow-clear
@input="handleSearch"
@keydown="handleKeydown"
ref="searchInputRef"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<div v-if="searchResults.length > 0" class="search-results">
<div
v-for="(item, index) in searchResults"
:key="item.path"
class="result-item"
:class="{ active: selectedIndex === index }"
@click="handleSelect(item)"
@mouseenter="selectedIndex = index"
>
<div class="result-icon">
<component :is="item.icon || 'MenuOutlined'" />
</div>
<div class="result-content">
<div class="result-title">{{ item.title }}</div>
<div v-if="item.breadcrumbs" class="result-path">{{ item.breadcrumbs }}</div>
</div>
</div>
</div>
<div v-else-if="searchKeyword" class="no-results">
<a-empty :description="$t('common.noResults')" />
</div>
<div v-else class="search-tips">
<div class="tip-title">{{ $t('common.searchTips') }}</div>
<div class="tip-list">
<div class="tip-item">
<kbd></kbd>
<kbd></kbd>
<span>{{ $t('common.navigateResults') }}</span>
</div>
<div class="tip-item">
<kbd>Enter</kbd>
<span>{{ $t('common.selectResult') }}</span>
</div>
<div class="tip-item">
<kbd>Esc</kbd>
<span>{{ $t('common.closeSearch') }}</span>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { SearchOutlined, MenuOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { useI18n } from 'vue-i18n'
// 定义组件名称
defineOptions({
name: 'MenuSearch',
})
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const visible = defineModel('visible', { type: Boolean, default: false })
const searchKeyword = ref('')
const searchResults = ref([])
const selectedIndex = ref(0)
const searchInputRef = ref(null)
// 将扁平化的菜单数据转换为可搜索格式
function flattenMenus(menus, breadcrumbs = []) {
const result = []
menus.forEach((menu) => {
if (menu.hidden) return
const currentBreadcrumbs = [...breadcrumbs, menu.title]
// 如果有路径且不是外部链接,添加到搜索结果
if (menu.path && !menu.path.startsWith('http')) {
result.push({
title: menu.title,
path: menu.path,
icon: menu.icon,
breadcrumbs: currentBreadcrumbs.join(' / '),
})
}
// 递归处理子菜单
if (menu.children && menu.children.length > 0) {
const children = flattenMenus(menu.children, currentBreadcrumbs)
result.push(...children)
}
})
return result
}
// 获取所有菜单项
const allMenus = computed(() => {
const menus = userStore.menu || []
return flattenMenus(menus)
})
// 执行搜索
function handleSearch() {
if (!searchKeyword.value.trim()) {
searchResults.value = []
selectedIndex.value = 0
return
}
const keyword = searchKeyword.value.toLowerCase().trim()
searchResults.value = allMenus.value.filter((menu) => {
return menu.title.toLowerCase().includes(keyword) ||
menu.breadcrumbs.toLowerCase().includes(keyword)
})
selectedIndex.value = 0
}
// 键盘导航
function handleKeydown(e) {
if (!searchResults.value.length) return
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
selectedIndex.value = selectedIndex.value > 0
? selectedIndex.value - 1
: searchResults.value.length - 1
break
case 'ArrowDown':
e.preventDefault()
selectedIndex.value = selectedIndex.value < searchResults.value.length - 1
? selectedIndex.value + 1
: 0
break
case 'Enter':
e.preventDefault()
if (searchResults.value[selectedIndex.value]) {
handleSelect(searchResults.value[selectedIndex.value])
}
break
case 'Escape':
e.preventDefault()
handleClose()
break
}
}
// 选择菜单项
function handleSelect(item) {
visible.value = false
router.push(item.path)
}
// 关闭搜索弹窗
function handleClose() {
visible.value = false
searchKeyword.value = ''
searchResults.value = []
selectedIndex.value = 0
}
// 监听弹窗显示,自动聚焦输入框
watch(visible, (newVal) => {
if (newVal) {
nextTick(() => {
searchInputRef.value?.focus()
})
} else {
handleClose()
}
})
</script>
<style scoped lang="scss">
.menu-search {
.search-results {
max-height: 400px;
overflow-y: auto;
margin-top: 16px;
border: 1px solid #f0f0f0;
border-radius: 4px;
.result-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:hover,
&.active {
background-color: #e6f7ff;
}
.result-icon {
margin-right: 12px;
font-size: 16px;
color: #1890ff;
}
.result-content {
flex: 1;
min-width: 0;
.result-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.result-path {
font-size: 12px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.no-results {
margin-top: 40px;
}
.search-tips {
margin-top: 20px;
.tip-title {
font-size: 14px;
color: #666;
margin-bottom: 16px;
text-align: center;
}
.tip-list {
display: flex;
flex-direction: row;
gap: 12px;
.tip-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background-color: #f5f5f5;
border-radius: 4px;
kbd {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
font-family: inherit;
line-height: 1;
color: #333;
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: 2px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
span {
font-size: 13px;
color: #666;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,448 @@
<template>
<a-drawer
v-model:open="visible"
:title="$t('common.taskCenter')"
placement="right"
:width="400"
:destroyOnClose="true"
>
<div class="task-drawer">
<!-- 任务统计 -->
<div class="task-stats">
<div class="stat-item">
<div class="stat-number">{{ totalTasks }}</div>
<div class="stat-label">{{ $t('common.totalTasks') }}</div>
</div>
<div class="stat-item">
<div class="stat-number pending">{{ pendingTasks }}</div>
<div class="stat-label">{{ $t('common.pendingTasks') }}</div>
</div>
<div class="stat-item">
<div class="stat-number completed">{{ completedTasks }}</div>
<div class="stat-label">{{ $t('common.completedTasks') }}</div>
</div>
</div>
<!-- 操作栏 -->
<div class="task-actions">
<a-input
v-model:value="searchKeyword"
:placeholder="$t('common.searchTasks')"
allow-clear
@input="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<div class="filter-buttons">
<a-button
:type="filterType === 'all' ? 'primary' : 'default'"
size="small"
@click="setFilter('all')"
>
{{ $t('common.all') }}
</a-button>
<a-button
:type="filterType === 'pending' ? 'primary' : 'default'"
size="small"
@click="setFilter('pending')"
>
{{ $t('common.pending') }}
</a-button>
<a-button
:type="filterType === 'completed' ? 'primary' : 'default'"
size="small"
@click="setFilter('completed')"
>
{{ $t('common.completed') }}
</a-button>
</div>
</div>
<!-- 任务列表 -->
<div class="task-list">
<div v-if="filteredTasks.length > 0">
<div
v-for="task in filteredTasks"
:key="task.id"
class="task-item"
:class="{ completed: task.completed }"
>
<div class="task-checkbox">
<a-checkbox
:checked="task.completed"
@change="toggleTask(task)"
/>
</div>
<div class="task-content">
<div class="task-title">{{ task.title }}</div>
<div class="task-meta">
<span class="task-priority" :class="task.priority">
{{ $t(`common.priority${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}`) }}
</span>
<span class="task-time">{{ task.time }}</span>
</div>
</div>
<div class="task-actions">
<a-popconfirm
:title="$t('common.confirmDelete')"
:ok-text="$t('common.confirm')"
:cancel-text="$t('common.cancel')"
@confirm="deleteTask(task.id)"
>
<DeleteOutlined class="action-icon" />
</a-popconfirm>
</div>
</div>
</div>
<a-empty v-else :description="$t('common.noTasks')" />
</div>
<!-- 底部操作 -->
<div class="drawer-footer">
<a-button @click="showAddTask">
<PlusOutlined />
{{ $t('common.addTask') }}
</a-button>
<a-button danger @click="clearAllTasks">
{{ $t('common.clearAll') }}
</a-button>
</div>
</div>
<!-- 添加任务弹窗 -->
<a-modal
v-model:open="addTaskVisible"
:title="$t('common.addTask')"
:ok-text="$t('common.confirm')"
:cancel-text="$t('common.cancel')"
@ok="confirmAddTask"
>
<a-form layout="vertical">
<a-form-item :label="$t('common.taskTitle')">
<a-input v-model:value="newTask.title" :placeholder="$t('common.enterTaskTitle')" />
</a-form-item>
<a-form-item :label="$t('common.taskPriority')">
<a-select v-model:value="newTask.priority">
<a-select-option value="low">{{ $t('common.priorityLow') }}</a-select-option>
<a-select-option value="medium">{{ $t('common.priorityMedium') }}</a-select-option>
<a-select-option value="high">{{ $t('common.priorityHigh') }}</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</a-drawer>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import { SearchOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
// 定义组件名称
defineOptions({
name: 'TaskDrawer',
})
const { t } = useI18n()
const visible = defineModel('visible', { type: Boolean, default: false })
const tasks = defineModel('tasks', { type: Array, default: () => [] })
// 搜索关键词
const searchKeyword = ref('')
// 筛选类型all, pending, completed
const filterType = ref('all')
// 添加任务弹窗
const addTaskVisible = ref(false)
const newTask = ref({
title: '',
priority: 'medium',
})
// 统计数据
const totalTasks = computed(() => tasks.value.length)
const pendingTasks = computed(() => tasks.value.filter(t => !t.completed).length)
const completedTasks = computed(() => tasks.value.filter(t => t.completed).length)
// 筛选后的任务列表
const filteredTasks = computed(() => {
let result = [...tasks.value]
// 按状态筛选
if (filterType.value === 'pending') {
result = result.filter(t => !t.completed)
} else if (filterType.value === 'completed') {
result = result.filter(t => t.completed)
}
// 按关键词搜索
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(t =>
t.title.toLowerCase().includes(keyword)
)
}
return result
})
// 切换任务状态
const toggleTask = (task) => {
task.completed = !task.completed
}
// 删除任务
const deleteTask = (id) => {
const index = tasks.value.findIndex(t => t.id === id)
if (index > -1) {
tasks.value.splice(index, 1)
message.success(t('common.deleted'))
}
}
// 清空所有任务
const clearAllTasks = () => {
tasks.value = []
message.success(t('common.cleared'))
}
// 显示添加任务弹窗
const showAddTask = () => {
newTask.value = {
title: '',
priority: 'medium',
}
addTaskVisible.value = true
}
// 确认添加任务
const confirmAddTask = () => {
if (!newTask.value.title.trim()) {
message.warning(t('common.pleaseEnterTaskTitle'))
return
}
tasks.value.unshift({
id: Date.now(),
title: newTask.value.title,
priority: newTask.value.priority,
completed: false,
time: t('common.justNow'),
})
addTaskVisible.value = false
message.success(t('common.added'))
}
// 设置筛选类型
const setFilter = (type) => {
filterType.value = type
}
// 搜索处理
const handleSearch = () => {
// 搜索逻辑在 computed 中自动处理
}
// 监听抽窗关闭,重置搜索和筛选
watch(visible, (newVal) => {
if (!newVal) {
searchKeyword.value = ''
filterType.value = 'all'
}
})
</script>
<style scoped lang="scss">
.task-drawer {
display: flex;
flex-direction: column;
height: 100%;
.task-stats {
display: flex;
gap: 16px;
margin-bottom: 20px;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
.stat-item {
flex: 1;
text-align: center;
.stat-number {
font-size: 28px;
font-weight: bold;
color: #fff;
margin-bottom: 4px;
&.pending {
color: #ffd666;
}
&.completed {
color: #95de64;
}
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
}
}
}
.task-actions {
margin-bottom: 16px;
:deep(.ant-input-affix-wrapper) {
margin-bottom: 12px;
}
.filter-buttons {
display: flex;
gap: 8px;
.ant-btn {
flex: 1;
}
}
}
.task-list {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
.task-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #f9f9f9;
border-radius: 6px;
transition: all 0.2s;
&:hover {
background-color: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
&.completed {
.task-title {
text-decoration: line-through;
color: #999;
}
.task-content {
opacity: 0.6;
}
}
.task-checkbox {
margin-right: 12px;
flex-shrink: 0;
}
.task-content {
flex: 1;
min-width: 0;
.task-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
.task-priority {
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
&.high {
background-color: #fff1f0;
color: #ff4d4f;
}
&.medium {
background-color: #fff7e6;
color: #fa8c16;
}
&.low {
background-color: #f6ffed;
color: #52c41a;
}
}
.task-time {
color: #999;
}
}
}
.task-actions {
margin-left: 8px;
flex-shrink: 0;
.action-icon {
font-size: 16px;
color: #999;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #ff4d4f;
}
}
}
}
}
.drawer-footer {
padding-top: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 12px;
.ant-btn {
flex: 1;
}
}
}
</style>

View File

@@ -34,28 +34,13 @@
</a-dropdown>
<!-- 任务列表 -->
<a-dropdown :trigger="['click']" placement="bottomRight">
<a-tooltip :title="$t('common.taskCenter')">
<a-badge :count="taskCount" :offset="[-5, 5]">
<a-button type="text" class="action-btn">
<a-button type="text" @click="taskVisible = true" class="action-btn">
<CheckSquareOutlined />
</a-button>
</a-badge>
<template #overlay>
<a-card class="dropdown-card" :title="$t('common.tasks')" :bordered="false">
<template #extra>
<a @click="clearTasks">{{ $t('common.clearAll') }}</a>
</template>
<div class="task-list">
<div v-for="task in tasks" :key="task.id" class="task-item">
<a-checkbox :checked="task.completed" @change="toggleTask(task)">
<span :class="{ completed: task.completed }">{{ task.title }}</span>
</a-checkbox>
</div>
<a-empty v-if="tasks.length === 0" :description="$t('common.noTasks')" />
</div>
</a-card>
</template>
</a-dropdown>
</a-tooltip>
<!-- 语言切换 -->
<a-dropdown :trigger="['click']" placement="bottomRight">
@@ -99,6 +84,10 @@
<SettingOutlined />
<span>{{ $t('common.systemSettings') }}</span>
</a-menu-item>
<a-menu-item key="clearCache">
<DeleteOutlined />
<span>{{ $t('common.clearCache') }}</span>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<LogoutOutlined />
@@ -108,6 +97,12 @@
</template>
</a-dropdown>
</div>
<!-- 菜单搜索弹窗 -->
<search v-model:visible="searchVisible" />
<!-- 任务抽屉 -->
<task v-model:visible="taskVisible" v-model:tasks="tasks" />
</template>
<script setup>
@@ -116,8 +111,10 @@ import { useRouter } from 'vue-router'
import { message, Modal } from 'ant-design-vue'
import { useUserStore } from '@/stores/modules/user'
import { useI18nStore } from '@/stores/modules/i18n'
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons-vue'
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
import search from './search.vue'
import task from './task.vue'
// 定义组件名称(多词命名)
defineOptions({
@@ -130,6 +127,8 @@ const userStore = useUserStore()
const i18nStore = useI18nStore()
const isFullscreen = ref(false)
const searchVisible = ref(false)
const taskVisible = ref(false)
// 消息数据
const messages = ref([
@@ -143,9 +142,9 @@ const messageCount = computed(() => messages.value.filter((m) => !m.read).length
// 任务数据
const tasks = ref([
{ id: 1, title: '完成用户审核', completed: false },
{ id: 2, title: '更新系统文档', completed: false },
{ id: 3, title: '优化数据库查询', completed: true },
{ id: 1, title: '完成用户审核', priority: 'high', completed: false, time: '今天' },
{ id: 2, title: '更新系统文档', priority: 'medium', completed: false, time: '明天' },
{ id: 3, title: '优化数据库查询', priority: 'low', completed: true, time: '昨天' },
])
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
@@ -176,7 +175,7 @@ onUnmounted(() => {
// 显示搜索功能
const showSearch = () => {
message.info('搜索功能开发中')
searchVisible.value = true
}
// 清除消息
@@ -185,15 +184,9 @@ const clearMessages = () => {
message.success(t('common.cleared'))
}
// 清除任务
const clearTasks = () => {
tasks.value = []
message.success(t('common.cleared'))
}
// 切换任务状态
const toggleTask = (task) => {
task.completed = !task.completed
// 显示任务抽屉
const showTasks = () => {
taskVisible.value = true
}
// 切换语言
@@ -209,8 +202,10 @@ const handleMenuClick = ({ key }) => {
router.push('/ucenter')
break
case 'settings':
// 系统设置功能暂未实现
message.info(t('common.settingsDeveloping'))
router.push('/system/setting')
break
case 'clearCache':
handleClearCache()
break
case 'logout':
handleLogout()
@@ -218,6 +213,40 @@ const handleMenuClick = ({ key }) => {
}
}
// 清除缓存
const handleClearCache = () => {
Modal.confirm({
title: t('common.confirmClearCache'),
content: t('common.clearCacheConfirm'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
try {
// 清除 localStorage
localStorage.clear()
// 清除 sessionStorage
sessionStorage.clear()
// 清除所有缓存
if ('caches' in window) {
caches.keys().then(names => {
names.forEach(name => {
caches.delete(name)
})
})
}
message.success(t('common.cacheCleared'))
// 延迟刷新页面以应用缓存清除
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) {
message.error(t('common.clearCacheFailed'))
console.error('清除缓存失败:', error)
}
},
})
}
// 退出登录
const handleLogout = () => {
Modal.confirm({

View File

@@ -0,0 +1,137 @@
<template>
<a-modal :open="visible" :title="isEdit ? $t('common.editConfig') : $t('common.addConfig')" :width="600"
:ok-text="$t('common.confirm')" :cancel-text="$t('common.cancel')" @ok="handleConfirm" @cancel="handleClose"
:after-close="handleClose">
<a-form ref="formRef" :model="formData" :label-col="{ span: 5 }">
<a-form-item v-if="!isEdit" :name="['category']" :label="$t('common.configCategory')"
:rules="[{ required: true, message: $t('common.pleaseSelect') + $t('common.configCategory') }]">
<a-select v-model:value="formData.category" :placeholder="$t('common.pleaseSelect')">
<a-select-option v-for="category in categories" :key="category.name" :value="category.name">
{{ category.title }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="!isEdit" :name="['name']" :label="$t('common.configName')"
:rules="[{ required: true, message: $t('common.pleaseEnter') + $t('common.configName') }]">
<a-input v-model:value="formData.name" :placeholder="$t('common.pleaseEnter')" />
</a-form-item>
<a-form-item :name="['title']" :label="$t('common.configTitle')"
:rules="[{ required: true, message: $t('common.pleaseEnter') + $t('common.configTitle') }]">
<a-input v-model:value="formData.title" :placeholder="$t('common.pleaseEnter')" />
</a-form-item>
<a-form-item v-if="!isEdit" :name="['type']" :label="$t('common.configType')"
:rules="[{ required: true, message: $t('common.pleaseSelect') + $t('common.configType') }]">
<a-select v-model:value="formData.type" :placeholder="$t('common.pleaseSelect')">
<a-select-option value="text">{{ $t('common.typeText') }}</a-select-option>
<a-select-option value="textarea">{{ $t('common.typeTextarea') }}</a-select-option>
<a-select-option value="number">{{ $t('common.typeNumber') }}</a-select-option>
<a-select-option value="switch">{{ $t('common.typeSwitch') }}</a-select-option>
<a-select-option value="select">{{ $t('common.typeSelect') }}</a-select-option>
<a-select-option value="multiselect">{{ $t('common.typeMultiselect') }}</a-select-option>
<a-select-option value="datetime">{{ $t('common.typeDatetime') }}</a-select-option>
<a-select-option value="color">{{ $t('common.typeColor') }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :name="['value']" :label="$t('common.configValue')">
<a-input v-model:value="formData.value" :placeholder="$t('common.pleaseEnter')" />
</a-form-item>
<a-form-item :name="['tip']" :label="$t('common.configTip')">
<a-textarea v-model:value="formData.tip" :placeholder="$t('common.pleaseEnter')" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
// 定义组件名称
defineOptions({
name: 'ConfigModal',
})
const { t } = useI18n()
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
// 是否为编辑模式
isEdit: {
type: Boolean,
default: false,
},
// 配置分类列表
categories: {
type: Array,
default: () => [],
},
// 编辑时的初始数据
initialData: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['update:visible', 'confirm'])
const formRef = ref(null)
// 表单数据
const formData = reactive({
category: '',
name: '',
title: '',
type: 'text',
value: '',
tip: '',
})
// 监听弹窗显示和初始数据变化
watch(() => props.initialData, (newVal) => {
if (props.isEdit && Object.keys(newVal).length > 0) {
formData.category = newVal.category || ''
formData.name = newVal.name || ''
formData.title = newVal.title || ''
formData.type = newVal.type || 'text'
formData.value = newVal.value || ''
formData.tip = newVal.tip || ''
}
}, { immediate: true })
// 监听弹窗关闭,重置表单
watch(() => props.visible, (newVal) => {
if (!newVal) {
handleClose()
}
})
// 确认提交
const handleConfirm = async () => {
try {
const values = await formRef.value.validate()
emit('confirm', values)
} catch (error) {
// 表单验证错误,不处理
}
}
// 关闭弹窗并重置表单
const handleClose = () => {
formRef.value?.resetFields()
formData.category = ''
formData.name = ''
formData.title = ''
formData.type = 'text'
formData.value = ''
formData.tip = ''
emit('update:visible', false)
}
</script>

View File

@@ -1,314 +1,338 @@
<template>
<div class="upload-demo">
<a-card title="图片上传组件示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="单图上传">
<ImageUpload v-model="singleImage" />
<div class="result">
<strong>结果</strong>
<p>{{ singleImage || '暂无图片' }}</p>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card type="inner" title="多图上传最多5张">
<ImageUpload
v-model="multipleImages"
:max-count="5"
@change="handleImageChange"
/>
<div class="result">
<strong>结果</strong>
<p v-if="multipleImages.length > 0">
{{ multipleImages.join(', ') }}
</p>
<p v-else>暂无图片</p>
</div>
</a-card>
</a-col>
</a-row>
<div class="system-setting">
<a-card :bordered="false">
<template #title>
<div class="page-title">
<SettingOutlined />
<span>{{ $t('common.systemSettings') }}</span>
</div>
</template>
<!-- Tab 页签 -->
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<template #rightExtra>
<a-button type="primary" @click="handleAddConfig">
<PlusOutlined />
{{ $t('common.addConfig') }}
</a-button>
</template>
<a-tab-pane v-for="category in categories" :key="category.name" :tab="category.title">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }" class="setting-form">
<a-form-item v-for="field in fields.filter(f => f.category === category.name)" :key="field.name"
:label="field.title">
<div class="form-item-content">
<div class="form-input-wrapper">
<!-- 文本输入 -->
<a-input v-if="field.type === 'text'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
<!-- 文本域 -->
<a-textarea v-else-if="field.type === 'textarea'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" :rows="4" />
<!-- 数字输入 -->
<a-input-number v-else-if="field.type === 'number'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')"
style="width: 100%" />
<!-- 开关 -->
<a-switch v-else-if="field.type === 'switch'"
v-model:checked="formData[field.name]" />
<!-- 下拉选择 -->
<a-select v-else-if="field.type === 'select'" v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 多选 -->
<a-select v-else-if="field.type === 'multiselect'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')" mode="multiple"
style="width: 100%">
<a-select-option v-for="option in field.options" :key="option.value"
:value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<!-- 日期时间 -->
<a-date-picker v-else-if="field.type === 'datetime'"
v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseSelect')"
style="width: 100%" show-time format="YYYY-MM-DD HH:mm:ss" />
<!-- 颜色选择器 -->
<a-input v-else-if="field.type === 'color'" v-model:value="formData[field.name]"
type="color" style="width: 100px" />
<!-- 默认文本输入 -->
<a-input v-else v-model:value="formData[field.name]"
:placeholder="field.placeholder || $t('common.pleaseEnter')" />
</div>
<div class="form-actions">
<EditOutlined class="action-icon edit-icon" :title="$t('common.edit')"
@click="handleEditField(field)" />
</div>
</div>
<div v-if="field.tip" class="field-tip">{{ field.tip }}</div>
</a-form-item>
</a-form>
<!-- 空状态 -->
<a-empty v-if="fields.filter(f => f.category === category.name).length === 0"
:description="$t('common.noConfig')" />
</a-tab-pane>
</a-tabs>
<!-- 底部保存按钮 -->
<div class="save-actions">
<a-space>
<a-button @click="handleReset">
{{ $t('common.reset') }}
</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">
<SaveOutlined />
{{ $t('common.save') }}
</a-button>
</a-space>
</div>
</a-card>
<a-card title="图片尺寸限制示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="限制图片尺寸 800x600">
<ImageUpload
v-model="sizeImage"
:min-width="800"
:max-width="1920"
:min-height="600"
:max-height="1080"
tip="尺寸要求800x600 ~ 1920x1080"
/>
<div class="result">
<strong>结果</strong>
<p>{{ sizeImage || '暂无图片' }}</p>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card type="inner" title="自定义上传文字">
<ImageUpload
v-model="customImage"
upload-text="点击选择图片"
tip="支持 JPG、PNG 格式,最大 10MB"
/>
<div class="result">
<strong>结果</strong>
<p>{{ customImage || '暂无图片' }}</p>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card title="上传事件监听示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="监听上传事件">
<ImageUpload
v-model="eventImage"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
@preview="handlePreview"
/>
<div class="result">
<strong>结果</strong>
<p>{{ eventImage || '暂无图片' }}</p>
<p v-if="eventLog" class="event-log">
<strong>事件日志</strong>
<pre>{{ eventLog }}</pre>
</p>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card title="文件上传组件示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="单文件上传">
<FileUpload v-model="singleFile" />
<div class="result">
<strong>结果</strong>
<p>{{ singleFile || '暂无文件' }}</p>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card type="inner" title="多文件上传">
<FileUpload
v-model="multipleFiles"
:max-count="10"
:multiple="true"
@change="handleFileChange"
@remove="handleFileRemove"
/>
<div class="result">
<strong>结果</strong>
<p v-if="multipleFiles.length > 0">
{{ multipleFiles.join(', ') }}
</p>
<p v-else>暂无文件</p>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card title="限制文件类型示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="仅支持 JPG/PNG 图片">
<ImageUpload
v-model="jpgImage"
accept="image/jpeg,image/png"
/>
</a-card>
</a-col>
<a-col :span="12">
<a-card type="inner" title="仅支持 PDF/Word 文档">
<FileUpload
v-model="documentFile"
accept=".pdf,.doc,.docx"
/>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card title="禁用状态示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="禁用图片上传">
<ImageUpload
v-model="disabledImage"
:disabled="true"
/>
</a-card>
</a-col>
<a-col :span="12">
<a-card type="inner" title="禁用文件上传">
<FileUpload
v-model="disabledFile"
:disabled="true"
/>
</a-card>
</a-col>
</a-row>
</a-card>
<a-card title="返回完整文件列表示例" class="demo-card">
<a-row :gutter="24">
<a-col :span="12">
<a-card type="inner" title="返回完整文件对象">
<ImageUpload
v-model="fullFileList"
:return-url="false"
@change="handleFullListChange"
/>
<div class="result">
<strong>完整文件列表</strong>
<pre>{{ JSON.stringify(fullFileList, null, 2) }}</pre>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
<!-- 配置弹窗 -->
<ConfigModal v-model:visible="modalVisible" :is-edit="isEditMode" :categories="categories"
:initial-data="currentEditData" @confirm="handleModalConfirm" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import ImageUpload from '@/components/scUpload/index.vue'
import FileUpload from '@/components/scUpload/file.vue'
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { SettingOutlined, PlusOutlined, EditOutlined, SaveOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
import systemApi from '@/api/system'
import ConfigModal from './components/ConfigModal.vue'
// 单图上传
const singleImage = ref('')
// 定义组件名称
defineOptions({
name: 'SystemSetting',
})
// 多图上传
const multipleImages = ref([])
const { t } = useI18n()
// 单文件上传
const singleFile = ref('')
const activeTab = ref('basic')
const saving = ref(false)
// 多文件上传
const multipleFiles = ref([])
// 配置分类
const categories = ref([
{ name: 'basic', title: '基础设置' },
{ name: 'security', title: '安全设置' },
{ name: 'upload', title: '上传设置' },
{ name: 'email', title: '邮件设置' },
{ name: 'sms', title: '短信设置' },
])
// 限制类型
const jpgImage = ref('')
const documentFile = ref('')
// 配置字段
const fields = ref([])
// 禁用状态
const disabledImage = ref('')
const disabledFile = ref('')
// 表单数据
const formData = reactive({})
// 完整文件列表
const fullFileList = ref([])
// 弹窗相关
const modalVisible = ref(false)
const isEditMode = ref(false)
const currentEditData = ref({})
// 新增示例
const sizeImage = ref('')
const customImage = ref('')
const eventImage = ref('')
const eventLog = ref('')
// 获取配置字段
const fetchFields = async () => {
try {
const res = await systemApi.setting.fields.get()
if (res.code === 200) {
fields.value = res.data.fields || []
categories.value = res.data.categories || categories.value
// 图片变化事件
const handleImageChange = (value, fileList) => {
console.log('图片URL数组:', value)
console.log('完整文件列表:', fileList)
// 初始化表单数据
fields.value.forEach(field => {
formData[field.name] = field.value || ''
})
// 设置第一个 tab 为默认激活
if (categories.value.length > 0) {
activeTab.value = categories.value[0].name
}
}
} catch (error) {
message.error(t('common.fetchConfigFailed'))
console.error('获取配置字段失败:', error)
}
}
// 文件变化事件
const handleFileChange = (value, fileList) => {
console.log('文件URL数组:', value)
console.log('完整文件列表:', fileList)
// 切换 Tab
const handleTabChange = (key) => {
activeTab.value = key
}
// 文件移除事件
const handleFileRemove = (file) => {
console.log('移除的文件:', file)
// 添加配置
const handleAddConfig = () => {
isEditMode.value = false
currentEditData.value = {
category: activeTab.value,
name: '',
title: '',
type: 'text',
value: '',
tip: '',
}
modalVisible.value = true
}
// 完整文件列表变化事件
const handleFullListChange = (value, fileList) => {
console.log('完整文件列表:', fileList)
// 编辑字段
const handleEditField = (field) => {
isEditMode.value = true
currentEditData.value = {
category: field.category,
name: field.name,
title: field.title,
type: field.type,
value: formData[field.name],
tip: field.tip || '',
}
modalVisible.value = true
}
// 上传成功事件
const handleUploadSuccess = (data, file) => {
eventLog.value = `上传成功\n文件名: ${file.name}\n响应数据: ${JSON.stringify(data, null, 2)}`
console.log('上传成功:', data, file)
// 弹窗确认处理
const handleModalConfirm = async (values) => {
try {
let res
if (isEditMode.value) {
// 编辑模式
res = await systemApi.setting.edit.post({
...values,
name: currentEditData.value.name,
})
if (res.code === 200) {
message.success(t('common.editSuccess'))
modalVisible.value = false
// 更新表单数据
formData[currentEditData.value.name] = values.value
// 更新字段信息
const fieldIndex = fields.value.findIndex(f => f.name === currentEditData.value.name)
if (fieldIndex > -1) {
fields.value[fieldIndex].title = values.title
fields.value[fieldIndex].value = values.value
fields.value[fieldIndex].tip = values.tip
}
}
} else {
// 添加模式
res = await systemApi.setting.add.post(values)
if (res.code === 200) {
message.success(t('common.addSuccess'))
modalVisible.value = false
// 重新获取配置字段
await fetchFields()
}
}
} catch (error) {
if (error.errorFields) {
return // 表单验证错误
}
message.error(isEditMode.value ? t('common.editFailed') : t('common.addFailed'))
console.error(isEditMode.value ? '编辑配置失败:' : '添加配置失败:', error)
}
}
// 上传失败事件
const handleUploadError = (errorMsg, file) => {
eventLog.value = `上传失败\n文件名: ${file.name}\n错误信息: ${errorMsg}`
console.log('上传失败:', errorMsg, file)
// 保存配置
const handleSave = async () => {
try {
saving.value = true
const res = await systemApi.setting.save.post(formData)
if (res.code === 200) {
message.success(t('common.saveSuccess'))
}
} catch (error) {
message.error(t('common.saveFailed'))
console.error('保存配置失败:', error)
} finally {
saving.value = false
}
}
// 预览事件
const handlePreview = (file) => {
eventLog.value = `预览图片\n文件名: ${file.name}\n状态: ${file.status}`
console.log('预览文件:', file)
// 重置配置
const handleReset = () => {
Object.keys(formData).forEach(key => {
const field = fields.value.find(f => f.name === key)
if (field) {
formData[key] = field.value || ''
}
})
message.info(t('common.resetSuccess'))
}
onMounted(() => {
fetchFields()
})
</script>
<style scoped>
.upload-demo {
padding: 24px;
<style scoped lang="scss">
.system-setting {
.page-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.setting-form {
margin-top: 20px;
.form-item-content {
display: flex;
align-items: center;
gap: 12px;
.form-input-wrapper {
flex: 1;
}
.form-actions {
.action-icon {
font-size: 16px;
color: #8c8c8c;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #1890ff;
}
}
}
}
.field-tip {
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
line-height: 1.4;
}
}
.save-actions {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
}
}
.demo-card {
margin-bottom: 24px;
:deep(.ant-tabs-tab) {
font-size: 14px;
}
.demo-card :deep(.ant-card-body) {
padding: 24px;
}
.demo-card :deep(.ant-card-head-title) {
font-size: 16px;
font-weight: 600;
}
.result {
margin-top: 16px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 4px;
}
.result p {
margin: 8px 0 0 0;
word-break: break-all;
}
.result pre {
margin: 8px 0 0 0;
max-height: 200px;
overflow-y: auto;
background: #fff;
padding: 8px;
border-radius: 4px;
}
.event-log {
margin-top: 12px !important;
padding: 8px !important;
background-color: #f0f2f5 !important;
border-radius: 4px;
}
.event-log pre {
margin: 8px 0 0 0;
max-height: 150px;
overflow-y: auto;
background: #fff;
padding: 8px;
border-radius: 4px;
font-size: 11px;
:deep(.ant-form-item) {
margin-bottom: 20px;
}
</style>