更新
This commit is contained in:
@@ -1,13 +1,341 @@
|
|||||||
import request from '../utils/request'
|
import request from "@/utils/request";
|
||||||
|
|
||||||
/**
|
export default {
|
||||||
* 用户登录
|
version: {
|
||||||
* @returns {Promise} 菜单数据
|
url: `system/index/version`,
|
||||||
*/
|
name: "获取最新版本号",
|
||||||
export function upload(params) {
|
get: async function () {
|
||||||
return request({
|
return await request.get(this.url);
|
||||||
url: '/system/file/upload',
|
},
|
||||||
method: 'post',
|
},
|
||||||
data: params
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,37 @@ export default {
|
|||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
searchMenu: 'Search Menu',
|
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',
|
messages: 'Messages',
|
||||||
tasks: 'Tasks',
|
tasks: 'Tasks',
|
||||||
clearAll: 'Clear All',
|
clearAll: 'Clear All',
|
||||||
@@ -62,6 +93,33 @@ export default {
|
|||||||
info: 'Info',
|
info: 'Info',
|
||||||
confirmDelete: 'Are you sure you want to delete?',
|
confirmDelete: 'Are you sure you want to delete?',
|
||||||
confirmLogout: 'Are you sure you want to logout?',
|
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',
|
required: 'This field is required',
|
||||||
operation: 'Operation',
|
operation: 'Operation',
|
||||||
time: 'Time',
|
time: 'Time',
|
||||||
|
|||||||
@@ -5,6 +5,37 @@ export default {
|
|||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
register: '注册',
|
register: '注册',
|
||||||
searchMenu: '搜索菜单',
|
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: '消息',
|
messages: '消息',
|
||||||
tasks: '任务',
|
tasks: '任务',
|
||||||
clearAll: '清空全部',
|
clearAll: '清空全部',
|
||||||
@@ -62,6 +93,33 @@ export default {
|
|||||||
info: '提示',
|
info: '提示',
|
||||||
confirmDelete: '确定要删除吗?',
|
confirmDelete: '确定要删除吗?',
|
||||||
confirmLogout: '确定要退出登录吗?',
|
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: '此项为必填项',
|
required: '此项为必填项',
|
||||||
operation: '操作',
|
operation: '操作',
|
||||||
time: '时间',
|
time: '时间',
|
||||||
|
|||||||
302
src/layouts/components/search.vue
Normal file
302
src/layouts/components/search.vue
Normal 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>
|
||||||
448
src/layouts/components/task.vue
Normal file
448
src/layouts/components/task.vue
Normal 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>
|
||||||
@@ -34,28 +34,13 @@
|
|||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
|
||||||
<!-- 任务列表 -->
|
<!-- 任务列表 -->
|
||||||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
<a-tooltip :title="$t('common.taskCenter')">
|
||||||
<a-badge :count="taskCount" :offset="[-5, 5]">
|
<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 />
|
<CheckSquareOutlined />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-badge>
|
</a-badge>
|
||||||
<template #overlay>
|
</a-tooltip>
|
||||||
<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-dropdown :trigger="['click']" placement="bottomRight">
|
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||||||
@@ -99,6 +84,10 @@
|
|||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
<span>{{ $t('common.systemSettings') }}</span>
|
<span>{{ $t('common.systemSettings') }}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="clearCache">
|
||||||
|
<DeleteOutlined />
|
||||||
|
<span>{{ $t('common.clearCache') }}</span>
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-divider />
|
<a-menu-divider />
|
||||||
<a-menu-item key="logout">
|
<a-menu-item key="logout">
|
||||||
<LogoutOutlined />
|
<LogoutOutlined />
|
||||||
@@ -108,6 +97,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单搜索弹窗 -->
|
||||||
|
<search v-model:visible="searchVisible" />
|
||||||
|
|
||||||
|
<!-- 任务抽屉 -->
|
||||||
|
<task v-model:visible="taskVisible" v-model:tasks="tasks" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -116,8 +111,10 @@ import { useRouter } from 'vue-router'
|
|||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { useI18nStore } from '@/stores/modules/i18n'
|
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 { useI18n } from 'vue-i18n'
|
||||||
|
import search from './search.vue'
|
||||||
|
import task from './task.vue'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -130,6 +127,8 @@ const userStore = useUserStore()
|
|||||||
const i18nStore = useI18nStore()
|
const i18nStore = useI18nStore()
|
||||||
|
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
|
const searchVisible = ref(false)
|
||||||
|
const taskVisible = ref(false)
|
||||||
|
|
||||||
// 消息数据
|
// 消息数据
|
||||||
const messages = ref([
|
const messages = ref([
|
||||||
@@ -143,9 +142,9 @@ const messageCount = computed(() => messages.value.filter((m) => !m.read).length
|
|||||||
|
|
||||||
// 任务数据
|
// 任务数据
|
||||||
const tasks = ref([
|
const tasks = ref([
|
||||||
{ id: 1, title: '完成用户审核', completed: false },
|
{ id: 1, title: '完成用户审核', priority: 'high', completed: false, time: '今天' },
|
||||||
{ id: 2, title: '更新系统文档', completed: false },
|
{ id: 2, title: '更新系统文档', priority: 'medium', completed: false, time: '明天' },
|
||||||
{ id: 3, title: '优化数据库查询', completed: true },
|
{ id: 3, title: '优化数据库查询', priority: 'low', completed: true, time: '昨天' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
|
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
|
||||||
@@ -176,7 +175,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 显示搜索功能
|
// 显示搜索功能
|
||||||
const showSearch = () => {
|
const showSearch = () => {
|
||||||
message.info('搜索功能开发中')
|
searchVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除消息
|
// 清除消息
|
||||||
@@ -185,15 +184,9 @@ const clearMessages = () => {
|
|||||||
message.success(t('common.cleared'))
|
message.success(t('common.cleared'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除任务
|
// 显示任务抽屉
|
||||||
const clearTasks = () => {
|
const showTasks = () => {
|
||||||
tasks.value = []
|
taskVisible.value = true
|
||||||
message.success(t('common.cleared'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换任务状态
|
|
||||||
const toggleTask = (task) => {
|
|
||||||
task.completed = !task.completed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换语言
|
// 切换语言
|
||||||
@@ -209,8 +202,10 @@ const handleMenuClick = ({ key }) => {
|
|||||||
router.push('/ucenter')
|
router.push('/ucenter')
|
||||||
break
|
break
|
||||||
case 'settings':
|
case 'settings':
|
||||||
// 系统设置功能暂未实现
|
router.push('/system/setting')
|
||||||
message.info(t('common.settingsDeveloping'))
|
break
|
||||||
|
case 'clearCache':
|
||||||
|
handleClearCache()
|
||||||
break
|
break
|
||||||
case 'logout':
|
case 'logout':
|
||||||
handleLogout()
|
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 = () => {
|
const handleLogout = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
|
|||||||
137
src/pages/system/setting/components/ConfigModal.vue
Normal file
137
src/pages/system/setting/components/ConfigModal.vue
Normal 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>
|
||||||
@@ -1,314 +1,338 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="upload-demo">
|
<div class="system-setting">
|
||||||
<a-card title="图片上传组件示例" class="demo-card">
|
<a-card :bordered="false">
|
||||||
<a-row :gutter="24">
|
<template #title>
|
||||||
<a-col :span="12">
|
<div class="page-title">
|
||||||
<a-card type="inner" title="单图上传">
|
<SettingOutlined />
|
||||||
<ImageUpload v-model="singleImage" />
|
<span>{{ $t('common.systemSettings') }}</span>
|
||||||
<div class="result">
|
|
||||||
<strong>结果:</strong>
|
|
||||||
<p>{{ singleImage || '暂无图片' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</template>
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
<!-- Tab 页签 -->
|
||||||
<a-card type="inner" title="多图上传(最多5张)">
|
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
||||||
<ImageUpload
|
<template #rightExtra>
|
||||||
v-model="multipleImages"
|
<a-button type="primary" @click="handleAddConfig">
|
||||||
:max-count="5"
|
<PlusOutlined />
|
||||||
@change="handleImageChange"
|
{{ $t('common.addConfig') }}
|
||||||
/>
|
</a-button>
|
||||||
<div class="result">
|
</template>
|
||||||
<strong>结果:</strong>
|
|
||||||
<p v-if="multipleImages.length > 0">
|
<a-tab-pane v-for="category in categories" :key="category.name" :tab="category.title">
|
||||||
{{ multipleImages.join(', ') }}
|
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }" class="setting-form">
|
||||||
</p>
|
<a-form-item v-for="field in fields.filter(f => f.category === category.name)" :key="field.name"
|
||||||
<p v-else>暂无图片</p>
|
: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>
|
</div>
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<a-card title="图片尺寸限制示例" class="demo-card">
|
<!-- 配置弹窗 -->
|
||||||
<a-row :gutter="24">
|
<ConfigModal v-model:visible="modalVisible" :is-edit="isEditMode" :categories="categories"
|
||||||
<a-col :span="12">
|
:initial-data="currentEditData" @confirm="handleModalConfirm" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import ImageUpload from '@/components/scUpload/index.vue'
|
import { message } from 'ant-design-vue'
|
||||||
import FileUpload from '@/components/scUpload/file.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 { t } = useI18n()
|
||||||
const multipleImages = ref([])
|
|
||||||
|
|
||||||
// 单文件上传
|
const activeTab = ref('basic')
|
||||||
const singleFile = ref('')
|
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 fields = ref([])
|
||||||
const documentFile = ref('')
|
|
||||||
|
|
||||||
// 禁用状态
|
// 表单数据
|
||||||
const disabledImage = ref('')
|
const formData = reactive({})
|
||||||
const disabledFile = ref('')
|
|
||||||
|
|
||||||
// 完整文件列表
|
// 弹窗相关
|
||||||
const fullFileList = ref([])
|
const modalVisible = ref(false)
|
||||||
|
const isEditMode = ref(false)
|
||||||
|
const currentEditData = ref({})
|
||||||
|
|
||||||
// 新增示例
|
// 获取配置字段
|
||||||
const sizeImage = ref('')
|
const fetchFields = async () => {
|
||||||
const customImage = ref('')
|
try {
|
||||||
const eventImage = ref('')
|
const res = await systemApi.setting.fields.get()
|
||||||
const eventLog = ref('')
|
if (res.code === 200) {
|
||||||
|
fields.value = res.data.fields || []
|
||||||
|
categories.value = res.data.categories || categories.value
|
||||||
|
|
||||||
// 图片变化事件
|
// 初始化表单数据
|
||||||
const handleImageChange = (value, fileList) => {
|
fields.value.forEach(field => {
|
||||||
console.log('图片URL数组:', value)
|
formData[field.name] = field.value || ''
|
||||||
console.log('完整文件列表:', fileList)
|
})
|
||||||
|
|
||||||
|
// 设置第一个 tab 为默认激活
|
||||||
|
if (categories.value.length > 0) {
|
||||||
|
activeTab.value = categories.value[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('common.fetchConfigFailed'))
|
||||||
|
console.error('获取配置字段失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件变化事件
|
// 切换 Tab
|
||||||
const handleFileChange = (value, fileList) => {
|
const handleTabChange = (key) => {
|
||||||
console.log('文件URL数组:', value)
|
activeTab.value = key
|
||||||
console.log('完整文件列表:', fileList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件移除事件
|
// 添加配置
|
||||||
const handleFileRemove = (file) => {
|
const handleAddConfig = () => {
|
||||||
console.log('移除的文件:', file)
|
isEditMode.value = false
|
||||||
|
currentEditData.value = {
|
||||||
|
category: activeTab.value,
|
||||||
|
name: '',
|
||||||
|
title: '',
|
||||||
|
type: 'text',
|
||||||
|
value: '',
|
||||||
|
tip: '',
|
||||||
|
}
|
||||||
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完整文件列表变化事件
|
// 编辑字段
|
||||||
const handleFullListChange = (value, fileList) => {
|
const handleEditField = (field) => {
|
||||||
console.log('完整文件列表:', fileList)
|
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) => {
|
const handleModalConfirm = async (values) => {
|
||||||
eventLog.value = `上传成功\n文件名: ${file.name}\n响应数据: ${JSON.stringify(data, null, 2)}`
|
try {
|
||||||
console.log('上传成功:', data, file)
|
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) => {
|
const handleSave = async () => {
|
||||||
eventLog.value = `上传失败\n文件名: ${file.name}\n错误信息: ${errorMsg}`
|
try {
|
||||||
console.log('上传失败:', errorMsg, file)
|
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) => {
|
const handleReset = () => {
|
||||||
eventLog.value = `预览图片\n文件名: ${file.name}\n状态: ${file.status}`
|
Object.keys(formData).forEach(key => {
|
||||||
console.log('预览文件:', file)
|
const field = fields.value.find(f => f.name === key)
|
||||||
|
if (field) {
|
||||||
|
formData[key] = field.value || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
message.info(t('common.resetSuccess'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchFields()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.upload-demo {
|
.system-setting {
|
||||||
padding: 24px;
|
.page-title {
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.demo-card {
|
gap: 8px;
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-card :deep(.ant-card-body) {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-card :deep(.ant-card-head-title) {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result {
|
:deep(.ant-tabs-tab) {
|
||||||
margin-top: 16px;
|
font-size: 14px;
|
||||||
padding: 12px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.result p {
|
:deep(.ant-form-item) {
|
||||||
margin: 8px 0 0 0;
|
margin-bottom: 20px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user