36 Commits

Author SHA1 Message Date
f9199f06f6 release: v3.7.0 2025-06-13 23:12:06 +08:00
a6d6bdb742 style: 调整部分样式 2025-06-13 23:03:43 +08:00
fa1291bda2 fix: 修复消息已读后计数未更新的问题 2025-06-13 22:54:39 +08:00
8c100e5753 fix: 修复部分行为验证码使用错误 2025-06-13 22:11:24 +08:00
47f4ca611e fix: 修复加载图标样式错乱 2025-06-13 22:10:19 +08:00
1ef1ba6ec8 refactor: 调整我的消息内容展示方式为弹窗 2025-06-08 22:13:20 +08:00
秋帆
bce9fa6938 fix: 弹窗公告显示问题 2025-06-08 13:06:11 +08:00
a239dbd1ea feat: 新增弹窗公告 2025-06-08 12:11:38 +08:00
oldR
aa14c41df0 fix(system/role): 全选时一级菜单未提交服务端 (#68) 2025-05-26 09:04:23 +08:00
oldR
f66f80fc56 fix: 修复菜单快捷搜索问题 (#67) 2025-05-25 10:42:45 +08:00
9faee319dd chore: 调整接口文档菜单图标 2025-05-21 22:28:36 +08:00
e2d436fb30 fix: 修复查看更多消息按钮无法点击的问题 2025-05-21 22:23:03 +08:00
672f93c52e Revert "perf: 为滚动相关事件补全 passive 参数 (#61)"
This reverts commit 8c700990a0.
2025-05-20 22:53:47 +08:00
abf3f13041 feat: 重构公告及消息,公告支持系统消息推送提醒、定时发布、置顶、记录读取状态 2025-05-20 22:37:06 +08:00
feef35f541 fix(schedule/job): 修复 Cron 表达式无法选中问题 2025-05-18 13:31:46 +08:00
04444a4bd8 fix: 修复首页项目展示错误 2025-05-17 23:06:32 +08:00
2018cf0ead fix(system/file): 修复权限码错误 2025-05-17 20:23:43 +08:00
5511c87773 fix(system/file): 修复权限码错误 2025-05-16 23:12:12 +08:00
abdd773886 feat(system/file): 新增文件夹导航、计算文件夹大小功能 2025-05-16 23:03:58 +08:00
lzzz0359
86fb09efaa fix: 导入用户上传组件button拼写错误 2025-05-16 01:48:44 +00:00
b680ee3fac refactor(system/file): 重构文件管理相关代码 2025-05-15 23:18:02 +08:00
dc66e9e62c fix(system/file): 修复文件批量删除接口传参错误
Closes #63
2025-05-14 23:03:36 +08:00
lzzz0359
1940f6aaa1 style: 调整 GiCellTags 折叠项水平居中样式 2025-05-14 06:56:00 +00:00
70e3b6dace fix(system/dict): 修复字典项如果不选择颜色,就不会显示标签的问题
Closes #IC6N05
2025-05-11 10:32:08 +08:00
5c689678da build: 更新项目版本号至3.7.0-SNAPSHOT 2025-05-08 21:14:26 +08:00
b05ec99d35 feat(system/smsConfig): 短信配置新增设为默认功能 2025-05-07 20:46:57 +08:00
c9fe54c2d6 style: 优化短信配置表单样式 2025-04-28 21:58:34 +08:00
5768d55654 style: 短信配置、客户端配置状态表单项调整为统一风格 2025-04-27 21:07:42 +08:00
4e167368c4 chore: 更新百度统计编号 2025-04-26 13:48:44 +08:00
chengang
a8986b93a8 fix: 修复GiForm中DateRangePicker无法正确赋值 2025-04-24 02:41:07 +00:00
Yous
55ce849b2e feat(system/sms): 短信渠道支持数据字典配置 (#62) 2025-04-23 18:00:44 +08:00
七喜
8c700990a0 perf: 为滚动相关事件补全 passive 参数 (#61) 2025-04-23 17:58:48 +08:00
dd1504204c feat(layout): 添加消息中心入口 2025-04-17 21:58:44 +08:00
luoqiz
70e2de3250 feat: 文件管理呈目录形式展示 (#60) 2025-04-16 13:41:50 +08:00
471f30e1e7 revert: 还原 终端 => 客户端(终端容易被误解) 2025-04-15 22:25:39 +08:00
d927d8f58a style: 统一部分搜索栏 2025-04-15 22:07:01 +08:00
74 changed files with 1346 additions and 373 deletions

View File

@@ -17,5 +17,5 @@ VITE_OPEN_DEVTOOLS = false
# 应用配置面板
VITE_APP_SETTING = true
# 端ID
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

View File

@@ -13,5 +13,5 @@ VITE_BASE = '/'
# 应用配置面板
VITE_APP_SETTING = true
# 端ID
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

View File

@@ -18,5 +18,5 @@ VITE_OPEN_DEVTOOLS = true
# 应用配置面板
VITE_APP_SETTING = false
# 端ID
# 客户端ID
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'

6
.gitignore vendored
View File

@@ -19,7 +19,13 @@ coverage
# Editor directories and files
# .vscode/*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.idea
*.suo
*.ntvs*

View File

@@ -1,3 +1,38 @@
## [v3.7.0](https://github.com/continew-org/continew-admin-ui/compare/v3.6.0...v3.7.0) (2025-06-13)
### ✨ 新特性
- 文件管理支持目录层级 (GitHub#60@luoqiz) ([70e2de3](https://github.com/continew-org/continew-admin-ui/commit/70e2de3250f887fedbc75b73b1822e666b8a5001))
- 添加消息中心入口 ([dd15042](https://github.com/continew-org/continew-admin-ui/commit/dd1504204c649a36b1266bd46c68d0ad3007e315))
- 短信渠道支持数据字典配置 (GitHub#62@Top2Hub) ([55ce849](https://github.com/continew-org/continew-admin-ui/commit/55ce849b2eb95639e1096d5bc2f78a7e410c11b1))
- 短信配置新增设为默认功能 ([b05ec99](https://github.com/continew-org/continew-admin-ui/commit/b05ec99d35b7491310a74d3d04751eed292a61b2))
- 新增文件夹导航、计算文件夹大小功能 ([abdd773](https://github.com/continew-org/continew-admin-ui/commit/abdd773886f216ea8de19e78b89b12ac99743d41))
- 重构公告及消息,公告支持系统消息推送提醒、定时发布、置顶、记录读取状态 ([abf3f13](https://github.com/continew-org/continew-admin-ui/commit/abf3f130416c7eb512851ccf9d68191faef938ee)) ([e2d436f](https://github.com/continew-org/continew-admin-ui/commit/e2d436fb3083652dda1ba0c4e05ee47f4536ac9b))
### 💎 功能优化
- 还原 终端 => 客户端(终端容易被误解) ([471f30e](https://github.com/continew-org/continew-admin-ui/commit/471f30e1e7464f32157b70c3cbd964d5c8286306))
- 统一部分搜索栏 ([d927d8f](https://github.com/continew-org/continew-admin-ui/commit/d927d8f58a536c8b30629c556e592b7fa2d28b38))
- 短信配置、客户端配置状态表单项调整为统一风格 ([5768d55](https://github.com/continew-org/continew-admin-ui/commit/5768d556546853e43c92360ca1fed7c3a3d2d013)) ([c9fe54c](https://github.com/continew-org/continew-admin-ui/commit/c9fe54c2d65d86fde45d8d073ea00c160c84460b))
- 调整 GiCellTags 折叠项水平居中样式 (Gitee#59@lzzz0359) ([1940f6a](https://github.com/continew-org/continew-admin-ui/commit/1940f6aaa179014c604e902331b24b0ef35f4c65))
- 重构文件管理相关代码 ([b680ee3](https://github.com/continew-org/continew-admin-ui/commit/b680ee3fac93224b46effecf5d3e25d778d2ec16)) ([5511c87](https://github.com/continew-org/continew-admin-ui/commit/5511c877731ea6868d4eaa324efe5f1252855143)) ([2018cf0](https://github.com/continew-org/continew-admin-ui/commit/2018cf0eade0e906532d08fe4344950dd297e97e))
- 调整接口文档菜单图标 ([9faee31](https://github.com/continew-org/continew-admin-ui/commit/9faee319dd2814b8e7ad600f2ecfe1cb782fd2c0))
### 🐛 问题修复
- 修复消息中心已读计数更新问题 ([50cd13e](https://github.com/continew-org/continew-admin-ui/commit/50cd13e2e54b50e88339164707d1bdcdd5716946))
- 修复全部已读调用接口错误 ([cd1b0b8](https://github.com/continew-org/continew-admin-ui/commit/cd1b0b8c0922d19072c677c8e60205a984c58605))
- 修复GiForm中DateRangePicker无法正确赋值 (Gitee#58@chengangi) ([a8986b9](https://github.com/continew-org/continew-admin-ui/commit/a8986b93a8e4327dad3bd3171fc76dde1a761d43))
- 修复字典项如果不选择颜色,就不会显示标签的问题 ([70e3b6d](https://github.com/continew-org/continew-admin-ui/commit/70e3b6dace0de619a86242d741e3ec9c04e5b863))
- 修复文件批量删除接口传参错误 ([dc66e9e](https://github.com/continew-org/continew-admin-ui/commit/dc66e9e62cf644309cddccb7757fd12a6450b25b))
- 修复导入用户上传组件button拼写错误 (Gitee#60@lzzz0359) ([86fb09e](https://github.com/continew-org/continew-admin-ui/commit/86fb09efaaa88299a72c62dd7a1587c918bb2e90))
- 修复菜单快捷搜索问题 (GitHub#67@oldR) ([f66f80f](https://github.com/continew-org/continew-admin-ui/commit/f66f80fc56de84bc846a068736d11849bd210163))
- 修复全选时一级菜单未提交服务端的问题 (GitHub#68@oldR) ([aa14c41](https://github.com/continew-org/continew-admin-ui/commit/aa14c41df05d702cd62a4e84cf69319a4f76d685))
- 修复加载图标样式错乱 ([47f4ca6](https://github.com/continew-org/continew-admin-ui/commit/47f4ca611e398bc860b4eaf8ab5d2ed0e1c10521))
- 修复部分行为验证码使用错误 ([8c100e5](https://github.com/continew-org/continew-admin-ui/commit/8c100e5753778b78a7cad03da06e59bafb4dbcee))
- 修复消息已读后计数未更新的问题 ([fa1291b](https://github.com/continew-org/continew-admin-ui/commit/fa1291bda2d43f3692dc1f441f4d4b3934d620f4))
## [v3.6.0](https://github.com/continew-org/continew-admin-ui/compare/v3.5.0...v3.6.0) (2025-04-13)
### ✨ 新特性
@@ -14,7 +49,7 @@
### 💎 功能优化
- 添加文件路径和md5值 (GitHub#52luoqiz) ([30821b5](https://github.com/continew-org/continew-admin-ui/commit/30821b551ca21c6bd13b2e1e0efdefd098ded099))
- 添加文件路径和md5值 (GitHub#52@luoqiz) ([30821b5](https://github.com/continew-org/continew-admin-ui/commit/30821b551ca21c6bd13b2e1e0efdefd098ded099))
- 优化角色权限节点关联及独立切换效果 ([657c83b](https://github.com/continew-org/continew-admin-ui/commit/657c83bf19b7d2997ddaf7466203441d56041765))
- 优化表单组件的字数限制逻辑 ([348c497](https://github.com/continew-org/continew-admin-ui/commit/348c49787618fabd23a040c77c4db53e4301bc61))
- 优化 GiForm、GiEditTable同步 GiDemo 更新) ([436cc6b](https://github.com/continew-org/continew-admin-ui/commit/436cc6bdfc2d4389b60181cadf6faf3c5a49cf7c))

View File

@@ -1,7 +1,7 @@
# ContiNew Admin UI
<a href="https://github.com/continew-org/continew-admin-ui" title="Release" target="_blank">
<img src="https://img.shields.io/badge/RELEASE-v3.6.0-%23ff3f59.svg" alt="Release" />
<img src="https://img.shields.io/badge/RELEASE-v3.7.0-%23ff3f59.svg" alt="Release" />
</a>
<a href="https://vuejs.org/" title="Vue" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.5.4-%236CB52D.svg?logo=Vue.js" alt="Vue" />
@@ -121,8 +121,8 @@ public class DeptController extends BaseController<DeptService, DeptResp, DeptDe
- 角色管理:管理系统用户的功能权限及数据权限,包含新增、修改、删除、分配角色等功能
- 菜单管理:管理系统菜单及按钮权限,支持多级菜单,动态路由,包含新增、修改、删除等功能
- 部门管理:管理系统组织架构,包含新增、修改、删除、导出等功能,以树形列表进行展示
- 通知公告:管理系统公告,支持设置公告的生效时间、终止时间、通知范围(所有人、指定用户)
- 文件管理管理系统文件支持上传、下载、预览目前支持图片、音视频、PDF、Word、Excel、PPT、重命名、切换视图列表、网格等功能
- 通知公告:管理系统公告,支持通知范围(所有人、指定用户)、通知方式(系统消息、登录弹窗)、定时发送、置顶设置
- 文件管理:管理系统文件及文件夹支持上传、下载、预览目前支持图片、音视频、PDF、Word、Excel、PPT、重命名、切换视图列表、网格等功能
- 字典管理:管理系统公用数据字典,例如:消息类型。支持字典标签背景色和排序等配置
- 系统配置:
- 网站配置提供修改系统标题、Logo、favicon、版权信息等基础配置功能以方便用户系统与其自身品牌形象保持一致
@@ -131,7 +131,7 @@ public class DeptController extends BaseController<DeptService, DeptResp, DeptDe
- 邮件配置:提供系统发件箱配置,也支持通过配置文件指定
- 短信配置:提供系统短信服务配置,也支持通过配置文件指定
- 存储配置:管理文件存储配置,支持本地存储、兼容 S3 协议对象存储
- 端配置:多认证管理,可设置不同的 token 有效期
- 客户端配置:多客户端PC端、小程序端等认证管理,可设置不同的 token 有效期
- 在线用户:管理当前登录用户,可一键踢除下线
- 日志管理:管理系统登录日志、操作日志,支持查看日志详情,包含请求头、响应头等报文信息
- 短信日志:管理系统短信发送日志,支持删除、导出

View File

@@ -11,7 +11,7 @@ export default function appInfo(): Plugin {
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`${bgGreen('ContiNew Admin v3.6.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/admin/faq.html')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
`${bold(green(`${bgGreen('ContiNew Admin v3.7.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('常见问题:')}${underline('https://continew.top/admin/faq.html')}\n${cyan('持续迭代优化的前后端分离中后台管理系统框架。')}`,
{
padding: 1,
margin: 1,

View File

@@ -25,7 +25,7 @@
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?be968d539e394cf45975b5117965eb10";
hm.src = "https://hm.baidu.com/hm.js?246a935992138d6770cabe711402315c";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();

View File

@@ -1,7 +1,7 @@
{
"name": "continew-admin-ui",
"type": "module",
"version": "3.6.0",
"version": "3.7.0",
"private": "true",
"scripts": {
"bootstrap": "pnpm install --registry=https://registry.npmmirror.com",

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@ export interface DashboardNoticeResp {
id: number
title: string
type: number
isTop: boolean
}
/** 仪表盘访问趋势类型 */

View File

@@ -5,7 +5,7 @@ export interface JobResp {
jobName: string
description?: string
triggerType: number
triggerInterval: string | number
triggerInterval: string
executorType: number
taskType: number
executorInfo: string

View File

@@ -5,27 +5,27 @@ export type * from './type'
const BASE_URL = '/system/client'
/** @desc 查询端列表 */
/** @desc 查询客户端列表 */
export function listClient(query: T.ClientPageQuery) {
return http.get<PageRes<T.ClientResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询端详情 */
/** @desc 查询客户端详情 */
export function getClient(id: string) {
return http.get<T.ClientDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增端 */
/** @desc 新增客户端 */
export function addClient(data: any) {
return http.post(`${BASE_URL}`, data)
}
/** @desc 修改端 */
/** @desc 修改客户端 */
export function updateClient(data: any, id: string) {
return http.put(`${BASE_URL}/${id}`, data)
}
/** @desc 删除端 */
/** @desc 删除客户端 */
export function deleteClient(id: string) {
return http.del(`${BASE_URL}`, { ids: [id] })
}

View File

@@ -5,6 +5,11 @@ export type * from './type'
const BASE_URL = '/system/file'
/** @desc 上传文件 */
export function uploadFile(data: FormData) {
return http.post(`${BASE_URL}/upload`, data)
}
/** @desc 查询文件列表 */
export function listFile(query: T.FilePageQuery) {
return http.get<PageRes<T.FileItem[]>>(`${BASE_URL}`, query)
@@ -16,11 +21,26 @@ export function updateFile(data: any, id: string) {
}
/** @desc 删除文件 */
export function deleteFile(id: string) {
return http.del(`${BASE_URL}`, { ids: [id] })
export function deleteFile(ids: string[]) {
return http.del(`${BASE_URL}`, { ids })
}
/** @desc 查询文件资源统计统计 */
export function getFileStatistics() {
return http.get<T.FileStatisticsResp>(`${BASE_URL}/statistics`)
}
/** @desc 根据sha256检测文件是否已经在服务器存在 */
export function checkFile(sha256: string) {
return http.get<T.FileItem>(`${BASE_URL}/check`, { fileHash: sha256 })
}
/** @desc 创建文件夹 */
export function createDir(parentPath: string, name: string) {
return http.post<T.FileItem>(`${BASE_URL}/dir`, { parentPath, originalName: name })
}
/** @desc 查询文件夹大小 */
export function calcDirSize(id: string) {
return http.get<T.FileDirCalcSizeResp>(`${BASE_URL}/dir/${id}/size`)
}

View File

@@ -9,6 +9,5 @@ export * from './storage'
export * from './option'
export * from './smsConfig'
export * from './smsLog'
export * from './message'
export * from './user-profile'
export * from './user-message'

View File

@@ -1,31 +0,0 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/message'
/** @desc 查询消息列表 */
export function listMessage(query: T.MessagePageQuery) {
return http.get<PageRes<T.MessageResp[]>>(`${BASE_URL}`, query)
}
/** @desc 删除消息 */
export function deleteMessage(ids: Array<string>) {
return http.del(`${BASE_URL}`, { ids })
}
/** @desc 标记已读 */
export function readMessage(ids: Array<string>) {
return http.patch(`${BASE_URL}/read`, { ids })
}
/** @desc 全部已读 */
export function readAllMessage() {
return http.patch(`${BASE_URL}/readAll`)
}
/** @desc 查询未读消息数量 */
export function getUnreadMessageCount() {
return http.get(`${BASE_URL}/unread`)
}

View File

@@ -12,7 +12,7 @@ export function listNotice(query: T.NoticePageQuery) {
/** @desc 查询公告详情 */
export function getNotice(id: string) {
return http.get<T.NoticeResp>(`${BASE_URL}/${id}`)
return http.get<T.NoticeDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 新增公告 */

View File

@@ -29,3 +29,8 @@ export function updateSmsConfig(data: any, id: string) {
export function deleteSmsConfig(id: string) {
return http.del(`${BASE_URL}`, { ids: [id] })
}
/** @desc 设置默认配置 */
export function setDefaultSmsConfig(id: string) {
return http.put(`${BASE_URL}/${id}/default`)
}

View File

@@ -178,17 +178,22 @@ export interface DictItemPageQuery extends DictItemQuery, PageQuery {
export interface NoticeResp {
id?: string
title?: string
content: string
type: string
noticeScope: number
noticeMethods?: Array<number>
isTiming: boolean
publishTime?: string
isTop: boolean
status?: number
type?: string
effectiveTime?: string
terminateTime?: string
noticeScope?: number
noticeUsers?: Array<string>
createUserString?: string
createTime?: string
updateUserString?: string
updateTime?: string
}
export type NoticeDetailResp = NoticeResp & {
createUserString: string
createTime: string
updateUserString: string
updateTime: string
}
export type NoticePreviewResp = NoticeDetailResp & {
content: string
}
export interface NoticeQuery {
title?: string
@@ -202,24 +207,26 @@ export interface NoticePageQuery extends NoticeQuery, PageQuery {
export interface FileItem {
id: string
name: string
originalName: string
size: number
url: string
parentPath: string
absPath: string
metadata: string
md5: string
path: string
sha256: string
contentType: string
metadata: string
thumbnailSize: number
thumbnailUrl: string
thumbnailName: string
thumbnailMetadata: string
thumbnailUrl: string
extension: string
type: number
storageId: string
storageName: string
createUserString: string
createTime: string
updateUserString: string
updateTime: string
updateUserString?: string
updateTime?: string
}
/** 文件资源统计信息 */
export interface FileStatisticsResp {
@@ -229,10 +236,14 @@ export interface FileStatisticsResp {
unit: string
data: Array<FileStatisticsResp>
}
/** 文件夹计算大小信息 */
export interface FileDirCalcSizeResp {
size: number
}
export interface FileQuery {
name?: string
originalName?: string
type?: string
absPath?: string
parentPath?: string
sort: Array<string>
}
export interface FilePageQuery extends FileQuery, PageQuery {
@@ -264,7 +275,7 @@ export interface StorageQuery {
sort: Array<string>
}
/** 端类型 */
/** 客户端类型 */
export interface ClientResp {
id: string
clientId: string
@@ -379,6 +390,7 @@ export interface SmsConfigResp {
maximum: string
supplierConfig: string
status: number
isDefault: boolean
createUser: string
createTime: string
updateUser: string
@@ -430,6 +442,7 @@ export interface MessageResp {
title: string
content: string
type: number
path: string
isRead: boolean
readTime?: string
createUserString?: string

View File

@@ -1,8 +1,50 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/user/message'
/** @desc 查询未读消息数量 */
export function getUnreadMessageCount() {
return http.get(`${BASE_URL}/unread`)
}
/** @desc 查询消息列表 */
export function listMessage(query: T.MessagePageQuery) {
return http.get<PageRes<T.MessageResp[]>>(`${BASE_URL}`, query)
}
/** @desc 获取用户消息详情 */
export function getUserMessage(id: number) {
return http.get<T.MessageResp>(`${BASE_URL}/${id}`)
}
/** @desc 删除消息 */
export function deleteMessage(ids: Array<string>) {
return http.del(`${BASE_URL}`, { ids })
}
/** @desc 标记已读 */
export function readMessage(ids: Array<string>) {
return http.patch(`${BASE_URL}/read`, { ids })
}
/** @desc 全部已读 */
export function readAllMessage() {
return http.patch(`${BASE_URL}/readAll`)
}
/** @desc 查询未读公告数量 */
export function getUnreadNoticeCount() {
return http.get(`${BASE_URL}/notice/unread`)
}
/** @desc 查询未读公告 ID 列表 */
export function getUnreadNoticeIds(method: string) {
return http.get<number[]>(`${BASE_URL}/notice/unread/${method}`)
}
/** @desc 分页查询用户公告 */
export function listUserNotice(query: T.NoticePageQuery) {
return http.get<PageRes<T.NoticeResp[]>>(`${BASE_URL}/notice`, query)

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1747794763752" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2080" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#85EA2D" p-id="2081"></path><path d="M322.368 346.496c-1.536 17.472 0.64 35.648-0.576 53.312-1.344 17.728-3.52 35.264-7.04 52.8-4.928 24.96-20.48 43.904-41.984 59.648 41.792 27.2 46.464 69.312 49.28 112 1.28 23.04 0.768 46.272 3.136 69.12 1.728 17.728 8.64 22.272 26.944 22.848 7.488 0.192 15.168 0 23.808 0v54.72c-54.144 9.28-98.816-6.08-109.824-51.968a314.816 314.816 0 0 1-6.72-51.2c-1.152-18.304 0.768-36.608-0.64-54.912-3.84-50.24-10.368-67.2-58.432-69.504v-62.4c3.584-0.768 6.912-1.408 10.432-1.792 26.368-1.344 37.632-9.472 43.328-35.456 2.752-14.528 4.352-29.312 4.928-44.288 1.92-28.544 1.152-57.664 6.08-86.016 6.912-40.768 32.128-60.416 74.048-62.784 11.84-0.64 23.808 0 37.376 0v55.872c-5.696 0.448-10.624 1.216-15.744 1.216-34.048-1.216-35.84 10.432-38.4 38.784z m65.6 129.536h-0.832a36.032 36.032 0 0 0-3.52 71.872h2.368a35.456 35.456 0 0 0 37.376-33.28v-1.92a36.032 36.032 0 0 0-35.392-36.672z m123.456 0a34.56 34.56 0 0 0-35.648 33.28c0 1.152 0 2.176 0.192 3.328 0 21.44 14.592 35.2 36.608 35.2 21.696 0 35.2-14.08 35.2-36.352-0.128-21.504-14.528-35.648-36.352-35.456z m126.4 0a36.8 36.8 0 0 0-37.44 35.648c0 20.032 16.128 36.224 36.224 36.224h0.384c18.112 3.136 36.48-14.4 37.632-35.456 0.96-19.52-16.768-36.48-36.8-36.48z m173.44 2.944c-22.848-0.96-34.24-8.64-40-30.336a219.52 219.52 0 0 1-6.464-42.304c-1.6-26.432-1.408-52.992-3.2-79.36-4.096-62.592-49.344-84.48-115.136-73.6v54.272c10.432 0 18.56 0 26.56 0.256 14.016 0.192 24.64 5.504 25.984 21.056 1.408 14.144 1.408 28.544 2.752 42.88 2.816 28.608 4.352 57.536 9.28 85.696 4.352 23.232 20.288 40.512 40.192 54.72-34.88 23.424-45.12 56.896-46.912 94.464-0.96 25.792-1.536 51.84-2.944 77.824-1.152 23.616-9.408 31.296-33.28 31.872-6.656 0.192-13.184 0.768-20.608 1.216v55.68c13.952 0 26.752 0.768 39.552 0 39.744-2.368 63.744-21.632 71.68-60.224 3.328-21.312 5.312-42.752 5.888-64.192 1.344-19.712 1.152-39.616 3.2-59.072 2.88-30.528 16.896-43.136 47.36-45.056a41.344 41.344 0 0 0 8.512-1.984v-62.4c-5.12-0.64-8.704-1.216-12.416-1.408z" fill="#173647" p-id="2082"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -6,7 +6,6 @@
<a-tag v-else-if="dictItem.extra === 'warning'" color="orangered">{{ dictItem.label }}</a-tag>
<a-tag v-else-if="dictItem.extra === 'error'" color="red">{{ dictItem.label }}</a-tag>
<a-tag v-else-if="dictItem.extra === 'default'" color="gray">{{ dictItem.label }}</a-tag>
<a-tag v-else :color="dictItem.extra">{{ dictItem.label }}</a-tag>
</template>
<script setup lang="ts">

View File

@@ -30,4 +30,8 @@ interface Props {
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
:deep(.arco-overflow-list-overflow) {
display: flex;
}
</style>

View File

@@ -23,7 +23,7 @@
<DateRangePicker
v-bind="(item.props as A.RangePickerInstance['$props'])"
:model-value="modelValue[item.field as keyof typeof modelValue]"
@update:model-value="valueChange($event, item.field)"
@update:model-value="updateValue($event, item.field)"
/>
</template>
<component

View File

@@ -46,3 +46,5 @@ export const OfficeTypes = ['ppt', 'pptx', 'doc', 'docx', 'xls', 'xlsx', 'pdf']
export const WordTypes = ['doc', 'docx']
export const ExcelTypes = ['xls', 'xlsx']
export const DirTypes = ['dir']

View File

@@ -7,21 +7,46 @@
<Main></Main>
<GiFooter v-if="appStore.copyrightDisplay" />
</a-layout>
<!-- 公告弹窗 -->
<NoticePopup ref="noticePopupRef" />
</a-layout>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Asider from './components/Asider/index.vue'
import Header from './components/Header/index.vue'
import Main from './components/Main.vue'
import Tabs from './components/Tabs/index.vue'
import GiFooter from '@/components/GiFooter/index.vue'
import NoticePopup from '@/views/user/message/components/NoticePopup.vue'
import { useAppStore } from '@/stores'
import { useDevice } from '@/hooks'
import { getToken } from '@/utils/auth'
defineOptions({ name: 'LayoutDefault' })
const appStore = useAppStore()
const { isMobile } = useDevice()
// 公告弹窗引用
const noticePopupRef = ref<InstanceType<typeof NoticePopup>>()
// 检查并显示未读公告
const checkAndShowNotices = () => {
const token = getToken()
// 如果有token检查未读公告
if (token) {
setTimeout(() => {
noticePopupRef.value?.open()
}, 1000) // 延迟1秒显示让页面先加载完成
}
}
onMounted(() => {
checkAndShowNotices()
})
</script>
<style scoped lang="scss">

View File

@@ -30,10 +30,15 @@
<Main></Main>
<GiFooter v-if="appStore.copyrightDisplay" />
</section>
<!-- 公告弹窗 -->
<NoticePopup ref="noticePopupRef" />
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { searchTree } from 'xe-utils'
import Main from './components/Main.vue'
@@ -44,10 +49,12 @@ import Logo from './components/Logo.vue'
import MenuFoldBtn from './components/MenuFoldBtn.vue'
import WwAds from './components/WwAds.vue'
import GiFooter from '@/components/GiFooter/index.vue'
import NoticePopup from '@/views/user/message/components/NoticePopup.vue'
import { useAppStore, useRouteStore } from '@/stores'
import { isExternal } from '@/utils/validate'
import { filterTree } from '@/utils'
import { useDevice } from '@/hooks'
import { getToken } from '@/utils/auth'
defineOptions({ name: 'LayoutMix' })
const route = useRoute()
@@ -63,6 +70,21 @@ const menuRoutes = filterTree(cloneRoutes, (i) => i.meta?.hidden === false)
const topMenus = ref<RouteRecordRaw[]>([])
topMenus.value = JSON.parse(JSON.stringify(menuRoutes))
// 公告弹窗引用
const noticePopupRef = ref<InstanceType<typeof NoticePopup>>()
// 检查并显示未读公告
const checkAndShowNotices = () => {
const token = getToken()
// 如果有token检查未读公告
if (token) {
setTimeout(() => {
noticePopupRef.value?.open()
}, 1000) // 延迟1秒显示让页面先加载完成
}
}
const getMenuIcon = (item: RouteRecordRaw) => {
return item.meta?.icon || item.children?.[0].meta?.icon
}
@@ -102,6 +124,10 @@ watch(
},
{ immediate: true },
)
onMounted(() => {
checkAndShowNotices()
})
</script>
<style scoped lang="scss">

View File

@@ -3,13 +3,13 @@
<a-list :loading="loading">
<template #header>通知</template>
<a-list-item v-for="item in messageList" :key="item.id">
<div class="content-wrapper" @click="open">
<div class="content-wrapper" @click="open(item.path)">
<div class="content">{{ item.title }}</div>
<div class="date">{{ item.createTime }}</div>
</div>
</a-list-item>
<template #footer>
<a class="more-btn" @click="open">查看更多
<a class="more-btn" @click="open()">查看更多
<icon-right />
</a>
<a class="read-all-btn" @click="readAll">全部已读</a>
@@ -48,7 +48,11 @@ const getMessageData = async () => {
}
// 打开消息中心
const open = () => {
const open = (path?: string) => {
if (path) {
router.push(path)
return
}
router.push({ path: '/user/message', query: { tab: 'msg' } })
}
@@ -104,6 +108,22 @@ onMounted(() => {
.arco-list-content {
max-height: 184px;
overflow-y: auto;
padding-right: 8px;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-text-4);
border-radius: 3px;
&:hover {
background-color: var(--color-text-3);
}
}
.arco-list-item {
padding: 6px;

View File

@@ -107,15 +107,14 @@ const searchRoutes = (keyword: string) => {
const result: SearchResult[] = []
const loop = (routes: RouteRecordRaw[]) => {
routes.forEach((route) => {
if (route.meta?.title?.toLowerCase().includes(keyword.toLowerCase()) && !route.meta?.hidden) {
result.push({
title: route.meta.title,
path: route.path,
})
}
if (route.children && route.children.length > 0) {
loop(route.children)
} else {
if (route.meta?.title?.toLowerCase().includes(keyword.toLowerCase())) {
result.push({
title: route.meta.title,
path: route.path,
})
}
}
})
}

View File

@@ -58,6 +58,9 @@
<a-doption @click="router.push('/user/profile')">
<span>个人中心</span>
</a-doption>
<a-doption @click="router.push('/user/message')">
<span>消息中心</span>
</a-doption>
<a-divider :margin="0" />
<a-doption @click="logout">
<span>退出登录</span>

View File

@@ -41,26 +41,26 @@ interface Props {
}
// 如果hidden: false那么代表这个路由项显示在左侧菜单栏中
// 如果props.item的子项chidren只有一个hidden: false的子元素, 那么onlyOneChild就表示这个子元素
// 如果props.item的子项children只有一个hidden: false的子元素, 那么onlyOneChild就表示这个子元素
const onlyOneChild = ref<RouteRecordRaw | null>(null)
const isOneShowingChild = ref(false)
const handleFunction = () => {
const chidrens = props.item?.children?.length ? props.item.children : []
const children = props.item?.children?.length ? props.item.children : []
// 判断是否只有一个显示的子项
const showingChildrens = chidrens.filter((i) => i.meta?.hidden === false)
if (showingChildrens.length) {
const showingChildren = children.filter((i) => i.meta?.hidden === false)
if (showingChildren.length) {
// 保存子项最后一个hidden: false的元素
onlyOneChild.value = showingChildrens[showingChildrens.length - 1]
onlyOneChild.value = showingChildren[showingChildren.length - 1]
}
// 当只有一个要显示子路由时, 默认显示该子路由器
if (showingChildrens.length === 1) {
if (showingChildren.length === 1) {
isOneShowingChild.value = true
}
// 如果没有要显示的子路由, 则显示父路由
if (showingChildrens.length === 0) {
if (showingChildren.length === 0) {
onlyOneChild.value = { ...props.item, meta: { ...props.item.meta, noShowingChildren: true } } as any
isOneShowingChild.value = true
}

View File

@@ -63,8 +63,8 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/user/notice',
name: 'UserNotice',
component: () => import('@/views/user/message/components/detail/index.vue'),
meta: { title: '公告详情' },
component: () => import('@/views/user/message/components/view/index.vue'),
meta: { title: '查看公告' },
},
],
},
@@ -78,7 +78,7 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/about/document/api',
component: () => import('@/views/about/document/api/index.vue'),
meta: { title: '接口文档', icon: 'continew', hidden: false, keepAlive: true },
meta: { title: '接口文档', icon: 'swagger', hidden: false, keepAlive: true },
},
{
path: 'https://continew.top',

View File

@@ -93,7 +93,7 @@ const columns: TableInstance['columns'] = [
{ title: '表名称', dataIndex: 'tableName', minWidth: 225, ellipsis: true, tooltip: true },
{ title: '描述', dataIndex: 'comment', ellipsis: true, tooltip: true },
{ title: '类名前缀', dataIndex: 'classNamePrefix', ellipsis: true, tooltip: true },
{ title: '作者名称', dataIndex: 'author' },
{ title: '作者名称', dataIndex: 'author', ellipsis: true, tooltip: true },
{ title: '所属模块', dataIndex: 'moduleName', ellipsis: true, tooltip: true },
{ title: '模块包名', dataIndex: 'packageName', ellipsis: true, tooltip: true },
{ title: '配置时间', dataIndex: 'createTime', width: 180 },

View File

@@ -15,7 +15,7 @@
<a-empty v-if="dataList.length === 0">暂无公告</a-empty>
<div v-else>
<div v-for="(item, idx) in dataList" :key="idx" class="item">
<GiCellTag :value="item.type" :dict="notice_type" />
<a-tag v-if="item.isTop" color="red">置顶</a-tag>
<a-link class="item-content" @click="onDetail(item.id)">
<a-typography-paragraph
:ellipsis="{
@@ -35,9 +35,6 @@
<script setup lang="ts">
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
import { useDict } from '@/hooks/app'
const { notice_type } = useDict('notice_type')
const dataList = ref<DashboardNoticeResp[]>([])
const loading = ref(false)

View File

@@ -32,7 +32,7 @@
<a-card-meta>
<template #title>
<a-space>
<img :src="item.logo" width="35px" height="25px" alt="logo" />
<img :src="item.logo" width="25px" height="25px" alt="logo" />
<a-typography-paragraph
:ellipsis="{
rows: 1,
@@ -128,7 +128,7 @@ const list = [
name: 'charles7c.github.io',
owner: 'charles7c',
desc: '基于 VitePress 构建的个人知识库/博客。扩展 VitePress 默认主题增加ICP备案号、公安备案号显示增加文章元数据信息原创标识、作者、发布时间、分类、标签显示增加文末版权声明增加 Gitalk 评论功能,主页美化、自动生成侧边栏、文章内支持 Mermaid 流程图、MD公式、MD脚注、增加我的标签、我的归档等独立页面以及浏览器滚条等细节优化。',
logo: 'https://blog.charles7c.top/logo.png',
logo: 'https://charles7c.top/logo.png',
url: 'https://github.com/Charles7c/charles7c.github.io/stargazers',
status: '归档',
statusColor: 'rgb(var(--warning-6))',

View File

@@ -96,6 +96,8 @@ const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
// 重置行为参数
VerifyRef.value.instance.refresh()
VerifyRef.value.show()
}

View File

@@ -12,7 +12,7 @@
<a-input v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear />
</a-form-item>
<a-form-item field="captcha" hide-label>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn"
:loading="captchaLoading"
@@ -25,7 +25,7 @@
</a-form-item>
<a-form-item>
<a-space direction="vertical" fill class="w-full">
<a-button disabled class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
<a-button class="btn" type="primary" :loading="loading" html-type="submit" size="large" long>立即登录</a-button>
</a-space>
</a-form-item>
<Verify
@@ -40,8 +40,8 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import type { BehaviorCaptchaReq } from '@/apis'
// import { type BehaviorCaptchaReq, getSmsCaptcha } from '@/apis'
// import type { BehaviorCaptchaReq } from '@/apis'
import { type BehaviorCaptchaReq, getSmsCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
@@ -113,12 +113,11 @@ const resetCaptcha = () => {
}
// 获取验证码
// eslint-disable-next-line unused-imports/no-unused-vars
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getSmsCaptcha(form.phone, captchaReq)
await getSmsCaptcha(form.phone, captchaReq)
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`

View File

@@ -11,12 +11,8 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.createUserString" placeholder="搜索登录用户" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input v-model="queryForm.ip" placeholder="搜索登录 IP 或地点" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.createUserString" placeholder="搜索登录用户" allow-clear @search="search" />
<a-input-search v-model="queryForm.ip" placeholder="搜索登录 IP 或地点" allow-clear @search="search" />
<DateRangePicker v-model="queryForm.createTime" @change="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>

View File

@@ -12,12 +12,8 @@
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.createUserString" placeholder="搜索操作人" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input v-model="queryForm.ip" placeholder="搜索操作 IP 或地点" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-input-search v-model="queryForm.createUserString" placeholder="搜索操作人" allow-clear @search="search" />
<a-input-search v-model="queryForm.ip" placeholder="搜索操作 IP 或地点" allow-clear @search="search" />
<DateRangePicker v-model="queryForm.createTime" @change="search" />
<a-button @click="reset">
<template #icon><icon-refresh /></template>

View File

@@ -54,13 +54,12 @@
field="triggerInterval"
:rules="[{ required: true, message: '请输入间隔时长' }]"
>
<a-input-number
<a-input
v-model="form.triggerInterval"
placeholder="请输入间隔时长"
:min="1"
>
<template #suffix>秒</template>
</a-input-number>
</a-input>
</a-form-item>
<a-form-item
v-else
@@ -259,7 +258,7 @@ const rules: FormInstance['rules'] = {
const [form, resetForm] = useResetReactive({
triggerType: 2,
triggerInterval: 60,
triggerInterval: '60',
taskType: 1,
routeKey: 4,
blockStrategy: 1,
@@ -281,10 +280,10 @@ const reset = () => {
const triggerTypeChange = () => {
switch (form.triggerType) {
case 2:
form.triggerInterval = 60
form.triggerInterval = '60'
break
case 3:
form.triggerInterval = ''
form.triggerInterval = '0 * * * * ?'
break
}
}

View File

@@ -18,7 +18,6 @@ import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addClient, getClient, updateClient } from '@/apis/system/client'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { DisEnableStatusList } from '@/constant/common'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
@@ -31,7 +30,7 @@ const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改端' : '新增端'))
const title = computed(() => (isUpdate.value ? '修改客户端' : '新增客户端'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { client_type, auth_type_enum } = useDict('auth_type_enum', 'client_type')
@@ -45,7 +44,7 @@ const [form, resetForm] = useResetReactive({
const columns: ColumnItem[] = reactive([
{
label: '端类型',
label: '客户端类型',
field: 'clientType',
type: 'select',
span: 12,
@@ -108,12 +107,14 @@ const columns: ColumnItem[] = reactive([
{
label: '状态',
field: 'status',
type: 'radio-group',
required: true,
type: 'switch',
span: 24,
props: {
type: 'button',
options: DisEnableStatusList,
type: 'round',
checkedValue: 1,
uncheckedValue: 2,
checkedText: '启用',
uncheckedText: '禁用',
},
},
])

View File

@@ -1,9 +1,9 @@
<template>
<a-drawer v-model:visible="visible" title="端详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-drawer v-model:visible="visible" title="客户端详情" :width="width >= 600 ? 600 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="端ID" :span="2"><a-typography-paragraph :copyable="!!dataDetail?.clientId">{{ dataDetail?.clientId }}</a-typography-paragraph></a-descriptions-item>
<a-descriptions-item label="端类型" :span="2">
<a-descriptions-item label="客户端ID" :span="2"><a-typography-paragraph :copyable="!!dataDetail?.clientId">{{ dataDetail?.clientId }}</a-typography-paragraph></a-descriptions-item>
<a-descriptions-item label="客户端类型" :span="2">
<GiCellTag :value="dataDetail?.clientType" :dict="client_type" />
</a-descriptions-item>
<a-descriptions-item label="认证类型" :span="2">

View File

@@ -15,7 +15,7 @@
<a-select
v-model="queryForm.clientType"
:options="client_type"
placeholder="请选择端类型"
placeholder="请选择客户端类型"
allow-clear
style="width: 160px"
@change="search"
@@ -109,7 +109,7 @@ const columns: TableInstance['columns'] = [
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '端 ID',
title: '客户端 ID',
dataIndex: 'clientId',
slotName: 'clientId',
ellipsis: true,
@@ -121,7 +121,7 @@ const columns: TableInstance['columns'] = [
},
},
{
title: '端类型',
title: '客户端类型',
dataIndex: 'clientType',
slotName: 'clientType',
ellipsis: true,
@@ -181,7 +181,7 @@ const reset = () => {
// 删除
const onDelete = (record: ClientResp) => {
return handleDelete(() => deleteClient(record.id), {
content: `是否确定删除端「${record.clientKey}(${record.clientId})」?`,
content: `是否确定删除客户端「${record.clientId}」?`,
showModal: true,
})
}

View File

@@ -57,7 +57,7 @@ const data = [
{ name: '邮件配置', key: 'mail', icon: 'email', permissions: ['system:mailConfig:get'], value: MailConfig },
{ name: '短信配置', key: 'sms', icon: 'message', permissions: ['system:smsConfig:list'], value: SmsConfig },
{ name: '存储配置', key: 'storage', icon: 'storage', permissions: ['system:storage:list'], value: StorageConfig },
{ name: '端配置', key: 'client', icon: 'mobile', permissions: ['system:client:list'], value: ClientConfig },
{ name: '客户端配置', key: 'client', icon: 'mobile', permissions: ['system:client:list'], value: ClientConfig },
]
const menuList = computed(() => {

View File

@@ -31,7 +31,7 @@ const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改短信配置' : '新增短信配置'))
const formRef = ref<InstanceType<typeof GiForm>>()
const { dis_enable_status_enum, sms_supplier_enum } = useDict('dis_enable_status_enum', 'sms_supplier_enum')
const { sms_supplier } = useDict('sms_supplier')
const [form, resetForm] = useResetReactive({
status: 1,
@@ -55,7 +55,7 @@ const columns: ColumnItem[] = reactive([
span: 12,
required: true,
props: {
options: sms_supplier_enum,
options: sms_supplier,
},
},
{
@@ -68,7 +68,7 @@ const columns: ColumnItem[] = reactive([
{
label: 'Secret Key',
field: 'secretKey',
type: 'input',
type: 'input-password',
span: 24,
required: true,
},
@@ -136,18 +136,23 @@ const columns: ColumnItem[] = reactive([
{
label: '厂商配置',
field: 'supplierConfig',
type: 'input',
type: 'textarea',
span: 24,
props: {
placeholder: '请输入 JSON 格式',
},
},
{
label: '状态',
field: 'status',
type: 'radio-group',
required: true,
type: 'switch',
span: 24,
props: {
type: 'button',
options: dis_enable_status_enum,
type: 'round',
checkedValue: 1,
uncheckedValue: 2,
checkedText: '启用',
uncheckedText: '禁用',
},
},
])

View File

@@ -16,7 +16,7 @@
<a-input-search v-model="queryForm.accessKey" placeholder="搜索 Access Key" allow-clear @search="search" />
<a-select
v-model="queryForm.supplier"
:options="sms_supplier_enum"
:options="sms_supplier"
placeholder="请选择厂商"
allow-clear
style="width: 150px"
@@ -33,8 +33,12 @@
<template #default>新增</template>
</a-button>
</template>
<template #isDefault="{ record }">
<a-tag v-if="record.isDefault" color="arcoblue"></a-tag>
<a-tag v-else color="red"></a-tag>
</template>
<template #supplier="{ record }">
<GiCellTag :value="record.supplier" :dict="sms_supplier_enum" />
<GiCellTag :value="record.supplier" :dict="sms_supplier" />
</template>
<template #accessKey="{ record }">
<CellCopy :content="record.accessKey" />
@@ -43,15 +47,26 @@
<a-space>
<a-link v-permission="['system:smsLog:list']" title="发送记录" @click="onLog(record)">发送记录</a-link>
<a-link v-permission="['system:smsConfig:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:smsConfig:delete']"
status="danger"
:disabled="record.disabled"
:title="record.disabled ? '不可删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
<a-dropdown>
<a-button v-if="has.hasPermOr(['system:smsConfig:setDefault', 'system:smsConfig:delete'])" type="text" size="mini" title="更多">
<template #icon>
<icon-more :size="16" />
</template>
</a-button>
<template #content>
<a-doption
v-permission="['system:smsConfig:setDefault']"
:title="record.isDefault ? '该配置已设为默认配置' : record.status === 2 ? '请先启用配置' : ''"
:disabled="record.isDefault || record.status === 2"
@click="onSetDefault(record)"
>
设为默认
</a-doption>
<a-doption v-permission="['system:smsConfig:delete']" :title="record.disabled ? '不可删除' : '删除'">
<a-link status="danger" :disabled="record.disabled" @click="onDelete(record)">删除</a-link>
</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
@@ -61,9 +76,16 @@
</template>
<script setup lang="tsx">
import { Message, Modal } from '@arco-design/web-vue'
import type { TableInstance } from '@arco-design/web-vue'
import SmsConfigAddModal from './SmsConfigAddModal.vue'
import { type SmsConfigQuery, type SmsConfigResp, deleteSmsConfig, listSmsConfig } from '@/apis/system/smsConfig'
import {
type SmsConfigQuery,
type SmsConfigResp,
deleteSmsConfig,
listSmsConfig,
setDefaultSmsConfig,
} from '@/apis/system/smsConfig'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
@@ -72,7 +94,7 @@ import GiCellStatus from '@/components/GiCell/GiCellStatus.vue'
defineOptions({ name: 'SystemSmsConfig' })
const { sms_supplier_enum } = useDict('sms_supplier_enum')
const { sms_supplier } = useDict('sms_supplier')
const queryForm = reactive<SmsConfigQuery>({
name: undefined,
@@ -97,7 +119,17 @@ const columns: TableInstance['columns'] = [
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '名称', dataIndex: 'name', slotName: 'name', width: 120, fixed: !isMobile() ? 'left' : undefined },
{ title: '厂商', dataIndex: 'supplier', slotName: 'supplier', width: 100 },
{
title: '厂商',
dataIndex: 'supplier',
slotName: 'supplier',
width: 100,
props: {
options: sms_supplier,
placeholder: '请选择厂商',
},
},
{ title: '是否默认', dataIndex: 'isDefault', slotName: 'isDefault', width: 100, align: 'center' },
{ title: 'Access Key', dataIndex: 'accessKey', slotName: 'accessKey', width: 200, ellipsis: true, tooltip: true },
{ title: 'Secret Key', dataIndex: 'secretKey', slotName: 'secretKey', width: 200, ellipsis: true, tooltip: true },
{ title: '短信签名', dataIndex: 'signature', slotName: 'signature', width: 200, ellipsis: true, tooltip: true },
@@ -134,7 +166,7 @@ const columns: TableInstance['columns'] = [
width: 200,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:smsLog:list', 'system:smsConfig:update', 'system:smsConfig:delete']),
show: has.hasPermOr(['system:smsLog:list', 'system:smsConfig:update', 'system:smsConfig:delete', 'system:smsConfig:setDefault']),
},
]
@@ -149,11 +181,33 @@ const reset = () => {
// 删除
const onDelete = (record: SmsConfigResp) => {
return handleDelete(() => deleteSmsConfig(record.id), {
content: `是否确定删除配置「${record.name}」?`,
content: `是否确定删除短信配置「${record.name}」?`,
showModal: true,
})
}
// 设为默认
const onSetDefault = (record: SmsConfigResp) => {
Modal.warning({
title: '提示',
content: `是否确定将短信配置「${record.name}」设为默认配置?`,
hideCancel: false,
maskClosable: false,
onBeforeOk: async () => {
try {
const res = await setDefaultSmsConfig(record.id)
if (res.success) {
Message.success('设置成功')
search()
}
return res.success
} catch (error) {
return false
}
},
})
}
const SmsConfigAddModalRef = ref<InstanceType<typeof SmsConfigAddModal>>()
// 新增
const onAdd = () => {

View File

@@ -101,7 +101,7 @@ const columns: ColumnItem[] = reactive([
field: 'domain',
type: 'input',
span: 24,
required: true,
required: false,
show: () => form.type === 2,
},
{

View File

@@ -1,6 +1,6 @@
<template>
<GiPageLayout>
<div class="header-actions">
<div>
<a-radio-group v-model="viewType" type="button" size="small" style="margin-bottom: 16px;">
<a-radio value="table">表格视图</a-radio>
<a-radio value="tree">组织架构图</a-radio>

View File

@@ -39,6 +39,7 @@
<a-tag v-else-if="record.color === 'warning'" color="orangered">{{ record.label }}</a-tag>
<a-tag v-else-if="record.color === 'error'" color="red">{{ record.label }}</a-tag>
<a-tag v-else-if="record.color === 'default'" color="gray">{{ record.label }}</a-tag>
<span v-else>{{ record.label }}</span>
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />

View File

@@ -5,7 +5,7 @@
<div ref="audioHeadRef" class="audio-box__header">
<div class="audio-name">
<icon-music :size="16" spin />
<span>{{ props.data?.name }}.{{ props.data?.extension }}</span>
<span>{{ props.data?.originalName }}</span>
</div>
<div class="close-icon" @click="close">
<icon-close :size="12" />

View File

@@ -7,35 +7,65 @@
<a-row style="margin-top: 15px">
<a-descriptions :column="1" layout="inline-vertical">
<a-descriptions-item label="名称">
<a-typography-paragraph copyable :copy-text="data.url">
<a-typography-paragraph :copyable="data.type !== 0" :copy-text="data.url">
<template #copy-tooltip>复制链接</template>
{{ getFileName(data) }}
{{ data.originalName }}
</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="大小">
<span v-if="data.type === 0" v-permission="['system:file:calcDirSize']">
<a-link
v-if="isCalculating || calculatedSize === null"
:disabled="isCalculating"
@click="calculateDirSize"
>
{{ isCalculating ? '计算中...' : '计算' }}
</a-link>
<span v-else>
{{ formatFileSize(calculatedSize) }}
</span>
</span>
<span v-else>{{ formatFileSize(data.size) }}</span>
</a-descriptions-item>
<a-descriptions-item label="路径">{{ `${data.parentPath === '/' ? '' : data.parentPath}/${data.name}` }}</a-descriptions-item>
<a-descriptions-item v-if="data.sha256" label="SHA256">
<a-typography-paragraph copyable :copy-text="data.sha256">
<template #copy-tooltip>复制</template>
{{ data.sha256 }}
</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="大小">{{ formatFileSize(data.size) }}</a-descriptions-item>
<a-descriptions-item label="路径">{{ data.absPath + getFileName(data) }}</a-descriptions-item>
<a-descriptions-item label="MD5">{{ data.md5 }}</a-descriptions-item>
<a-descriptions-item label="上传时间">{{ data.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ data.updateTime }}</a-descriptions-item>
<a-descriptions-item v-if="data?.updateTime" label="修改时间">{{ data?.updateTime }}</a-descriptions-item>
<a-descriptions-item label="存储名称">{{ data.storageName }}</a-descriptions-item>
</a-descriptions>
</a-row>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import FileImage from '../../main/FileMain/FileImage.vue'
import type { FileItem } from '@/apis/system'
import { type FileItem, calcDirSize } from '@/apis/system'
import { formatFileSize } from '@/utils'
interface Props {
data: FileItem
}
withDefaults(defineProps<Props>(), {})
// 文件名称带后缀
const getFileName = (item: FileItem) => {
return `${item.name}${item.extension ? `.${item.extension}` : ''}`
const props = withDefaults(defineProps<Props>(), {})
const isCalculating = ref(false)
const calculatedSize = ref<number | null>(null)
// 计算文件夹大小
const calculateDirSize = async () => {
if (isCalculating.value || props.data.type !== 0) return
isCalculating.value = true
try {
const { data } = await calcDirSize(props.data.id)
calculatedSize.value = data.size
} catch (err) {
Message.error('计算失败,请重试')
} finally {
isCalculating.value = false
}
}
</script>

View File

@@ -2,12 +2,12 @@
<a-row justify="center" align="center" style="padding: 0 5%">
<a-form ref="formRef" :model="form" auto-label-width class="w-full">
<a-form-item
label="文件名称"
field="name"
:rules="[{ required: true, message: '请输入文件名称' }]"
label="名称"
field="originalName"
:rules="[{ required: true, message: '请输入名称' }]"
style="margin-bottom: 0"
>
<a-input v-model="form.name" placeholder="请输入文件名称" allow-clear />
<a-input v-model="form.originalName" placeholder="请输入名称" allow-clear />
</a-form-item>
</a-form>
</a-row>
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<Props>(), {})
const formRef = ref<FormInstance>()
const form = reactive({
name: props.data?.name || '',
originalName: props.data?.originalName || '',
})
defineExpose({ formRef })

View File

@@ -21,7 +21,7 @@ export function openFileRenameModal(data: FileItem, callback?: () => void) {
const isInvalid = await ModalContentRef.value?.formRef?.validate()
const modelParams = ModalContentRef.value?.formRef?.model
if (isInvalid) return false
await updateFile({ name: modelParams?.name }, data.id)
await updateFile({ originalName: modelParams?.originalName }, data.id)
Message.success('重命名成功')
if (callback) {
callback()

View File

@@ -13,12 +13,12 @@
scroll-to-close
>
<a-grid-item>
<div class="file-grid-item" @click.stop="handleClickFile(item)">
<div class="file-grid-item" @click.stop="handleClickFile(item)" @dblclick="handleDblclickFile(item)">
<section class="file-grid-item__wrapper">
<div class="file-icon">
<FileImage :data="item" :title="item.name"></FileImage>
<FileImage :data="item" :title="item.originalName"></FileImage>
</div>
<p class="gi_line_1 file-name">{{ getFileName(item) }}</p>
<p class="gi_line_1 file-name">{{ item.originalName }}</p>
</section>
<!-- 勾选模式 -->
<section
@@ -31,7 +31,7 @@
</section>
</div>
</a-grid-item>
<template #content>
<template v-if="has.hasPermOr(['system:file:update', 'system:file:get', 'system:file:download', 'system:file:delete'])" #content>
<FileRightMenu :data="item" @click="handleRightMenuClick($event, item)"></FileRightMenu>
</template>
</a-trigger>
@@ -42,6 +42,7 @@
<script setup lang="ts">
import FileRightMenu from './FileRightMenu.vue'
import type { FileItem } from '@/apis/system'
import has from '@/utils/has'
const props = withDefaults(defineProps<Props>(), {
data: () => [], // 文件数据
@@ -51,6 +52,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
(e: 'click', record: FileItem): void
(e: 'dblclick', record: FileItem): void
(e: 'select', record: FileItem): void
(e: 'right-menu-click', mode: string, item: FileItem): void
}>()
@@ -63,16 +65,16 @@ interface Props {
isBatchMode?: boolean
}
// 文件名称带后缀
const getFileName = (item: FileItem) => {
return `${item.name}${item.extension ? `.${item.extension}` : ''}`
}
// 点击事件
const handleClickFile = (item: FileItem) => {
emit('click', item)
}
// 双击事件
const handleDblclickFile = (item: FileItem) => {
emit('dblclick', item)
}
// 选中事件
const handleCheckFile = (item: FileItem) => {
emit('select', item)

View File

@@ -15,13 +15,17 @@ const props = withDefaults(defineProps<Props>(), {})
// 是否是图片类型文件
const isImage = computed(() => {
const extension = props.data.extension.toLowerCase()
const extension = props.data.extension?.toLowerCase()
return ImageTypes.includes(extension)
})
// 获取文件图标,如果是图片就显示图片
const getFileImg = computed<string>(() => {
const extension = props.data.extension.toLowerCase()
// 文件夹
if (props.data.type === 0) {
return FileIcon.dir
}
const extension = props.data.extension?.toLowerCase()
if (ImageTypes.includes(extension)) {
return props.data.url || ''
}

View File

@@ -10,7 +10,6 @@
:selected-keys="selectedFileIds"
column-resizable
@select="select"
@row-click="handleRowClick"
>
<template #columns>
<a-table-column title="名称">
@@ -24,32 +23,41 @@
update-at-scroll
scroll-to-close
>
<section class="file-name">
<section class="file-name" @click="handleClick(record)" @dblclick="handleDblclickFile(record)">
<div class="file-image">
<FileImage :data="record"></FileImage>
</div>
<a-typography-paragraph copyable :copy-text="record.url">
<a-typography-paragraph :copyable="record.type !== 0" :copy-text="record.url">
<template #copy-tooltip>复制链接</template>
{{ getFileName(record) }}
{{ record.originalName }}
</a-typography-paragraph>
</section>
<template #content>
<template v-if="has.hasPermOr(['system:file:update', 'system:file:get', 'system:file:download', 'system:file:delete'])" #content>
<FileRightMenu :data="record" @click="handleRightMenuClick($event, record)"></FileRightMenu>
</template>
</a-trigger>
</template>
</a-table-column>
<a-table-column title="大小" data-index="size" :width="150">
<template #cell="{ record }">{{ formatFileSize(record.size) }}</template>
<a-table-column title="大小" data-index="size" :width="160">
<template #cell="{ record }">
<span v-if="record.type === 0" v-permission="['system:file:calcDirSize']">
<a-link v-if="record.size === null" @click="calculateDirSize(record)">计算</a-link>
<span v-else>
{{ formatFileSize(record.size) }}
</span>
</span>
<span v-else>{{ formatFileSize(record.size) }}</span>
</template>
</a-table-column>
<a-table-column title="存储名称" data-index="storageName" :width="200" />
<a-table-column title="修改时间" data-index="updateTime" :width="200" />
<a-table-column title="操作" :width="120" align="center">
<a-table-column v-if="has.hasPermOr(['system:file:update', 'system:file:get', 'system:file:download', 'system:file:delete'])" title="操作" :width="120" align="center">
<template #cell="{ record }">
<a-popover trigger="click" position="bottom" :content-style="{ 'padding': 0, 'margin-top': 0 }">
<a-button type="text" @click.stop><icon-more :size="16" /></a-button>
<template #content>
<FileRightMenu
:data="record"
:file-info="record"
:shadow="false"
@click="handleRightMenuClick($event, record)"
@@ -64,10 +72,11 @@
</template>
<script setup lang="ts">
import type { TableInstance, TableRowSelection } from '@arco-design/web-vue'
import { Message, type TableInstance, type TableRowSelection } from '@arco-design/web-vue'
import FileRightMenu from './FileRightMenu.vue'
import type { FileItem } from '@/apis/system'
import { type FileItem, calcDirSize } from '@/apis/system'
import { formatFileSize } from '@/utils'
import has from '@/utils/has'
const props = withDefaults(defineProps<Props>(), {
data: () => [], // 文件数据
@@ -77,6 +86,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
(e: 'click', record: FileItem): void
(e: 'dblclick', record: FileItem): void
(e: 'select', record: FileItem): void
(e: 'right-menu-click', mode: string, item: FileItem): void
}>()
@@ -89,26 +99,37 @@ interface Props {
isBatchMode?: boolean
}
// 文件名称带后缀
const getFileName = (item: FileItem) => {
return `${item.name}${item.extension ? `.${item.extension}` : ''}`
}
const rowSelection: TableRowSelection = reactive({
type: 'checkbox',
showCheckedAll: true,
})
// 计算文件夹大小
const calculateDirSize = async (record: FileItem) => {
if (record.type !== 0) return
try {
const { data } = await calcDirSize(record.id)
record.size = data.size
} catch (err) {
Message.error('计算失败,请重试')
}
}
// 多选
const select: TableInstance['onSelect'] = (rowKeys, rowKey, record) => {
emit('select', record as unknown as FileItem)
}
// 行点击事件
const handleRowClick: TableInstance['onRowClick'] = (record) => {
// 击事件
const handleClick = (record) => {
emit('click', record as unknown as FileItem)
}
// 双击事件
const handleDblclickFile = (item: FileItem) => {
emit('dblclick', item)
}
// 右键菜单点击事件
const handleRightMenuClick = (mode: string, item: FileItem) => {
emit('right-menu-click', mode, item)

View File

@@ -1,9 +1,9 @@
<template>
<GiOption :class="{ shadow: props.shadow }">
<GiOptionItem label="重命名" @click="onClickItem('rename')"> </GiOptionItem>
<GiOptionItem label="详情" @click="onClickItem('detail')"> </GiOptionItem>
<GiOptionItem label="下载" @click="onClickItem('download')"></GiOptionItem>
<GiOptionItem label="删除" @click="onClickItem('delete')"> </GiOptionItem>
<GiOptionItem v-permission="['system:file:update']" label="重命名" @click="onClickItem('rename')"> </GiOptionItem>
<GiOptionItem v-permission="['system:file:get']" label="详情" @click="onClickItem('detail')"> </GiOptionItem>
<GiOptionItem v-if="data?.type !== 0" v-permission="['system:file:download']" label="下载" @click="onClickItem('download')"></GiOptionItem>
<GiOptionItem v-permission="['system:file:delete']" label="删除" @click="onClickItem('delete')"> </GiOptionItem>
</GiOption>
</template>

View File

@@ -1,30 +1,30 @@
<template>
<div class="file-main">
<!-- 目录导航面包屑 -->
<a-breadcrumb class="file-main__breadcrumb">
<a-breadcrumb-item v-if="queryForm.parentPath" @click="handleBreadcrumbClick({ name: '根目录', path: '/' })">根目录</a-breadcrumb-item>
<a-breadcrumb-item v-else>全部</a-breadcrumb-item>
<a-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index" @click="handleBreadcrumbClick(item)">
{{ item.name || '根目录' }}
</a-breadcrumb-item>
</a-breadcrumb>
<a-row justify="space-between" class="file-main__search">
<!-- 左侧区域 -->
<a-space wrap>
<a-dropdown>
<a-upload :show-file-list="false" :custom-request="handleUpload">
<template #upload-button>
<a-button type="primary" shape="round">
<template #icon>
<icon-upload />
</template>
<template #default>上传</template>
</a-button>
</template>
</a-upload>
</a-dropdown>
<a-upload v-permission="['system:file:upload']" :show-file-list="false" :custom-request="handleUpload">
<template #upload-button>
<a-button type="primary" shape="round">
<template #icon>
<icon-upload />
</template>
<template #default>上传</template>
</a-button>
</template>
</a-upload>
<a-input-group>
<a-input
v-model="queryForm.absPath" placeholder="路径" allow-clear style="width: 300px"
@change="search"
/>
<a-input
v-model="queryForm.name" placeholder="搜索文件名" allow-clear style="width: 200px"
@change="search"
/>
<a-input v-model="queryForm.originalName" :placeholder="queryForm.type && queryForm.type !== '0' ? '请输入名称' : '在当前目录下搜索名称'" allow-clear style="width: 200px" />
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
@@ -44,7 +44,13 @@
<icon-delete />
</template>
</a-button>
<a-button type="primary" @click="isBatchMode = !isBatchMode">
<a-button v-permission="['system:file:createDir']" type="primary" :disabled="!queryForm.parentPath" @click="createDirModalVisible = !createDirModalVisible">
<template #icon>
<icon-folder />
</template>
<template #default>新建文件夹</template>
</a-button>
<a-button v-permission="['system:file:delete']" type="primary" @click="isBatchMode = !isBatchMode">
<template #icon>
<icon-select-all />
</template>
@@ -68,14 +74,14 @@
<FileGrid
v-show="fileList.length && mode === 'grid'" :data="fileList" :is-batch-mode="isBatchMode"
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"
@right-menu-click="handleRightMenuClick" @dblclick="handleDblclickFile"
></FileGrid>
<!-- 文件列表-列表模式 -->
<FileList
v-show="fileList.length && mode === 'list'" :data="fileList" :is-batch-mode="isBatchMode"
:selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile"
@right-menu-click="handleRightMenuClick"
@right-menu-click="handleRightMenuClick" @dblclick="handleDblclickFile"
></FileList>
<a-empty v-if="!fileList.length" />
@@ -84,6 +90,11 @@
<div class="pagination">
<a-pagination v-bind="pagination" />
</div>
<!-- 弹出新建窗口 -->
<a-modal v-model:visible="createDirModalVisible" title="新建文件夹" @ok="handleCreateDir" @cancel="handleCancel">
<a-input v-model="newDirName" placeholder="请输入文件夹名称" size="large" allow-clear />
</a-modal>
</div>
</template>
@@ -99,7 +110,7 @@ import {
import FileGrid from './FileGrid.vue'
import useFileManage from './useFileManage'
import { useTable } from '@/hooks'
import { type FileItem, type FileQuery, deleteFile, listFile, uploadFile } from '@/apis'
import { type FileItem, type FileQuery, createDir, deleteFile, listFile, uploadFile } from '@/apis/system/file'
import { ImageTypes, OfficeTypes } from '@/constant/file'
import 'viewerjs/dist/viewer.css'
import { downloadByUrl } from '@/utils/downloadFile'
@@ -113,11 +124,12 @@ const route = useRoute()
const { mode, selectedFileIds, toggleMode, addSelectedFileItem } = useFileManage()
const queryForm = reactive<FileQuery>({
name: undefined,
absPath: undefined,
type: route.query.type?.toString() !== '0' ? route.query.type?.toString() : undefined,
sort: ['updateTime,desc'],
originalName: undefined,
parentPath: (!route.query.type || route.query.type?.toString() === '0') ? '/' : undefined,
type: route.query.type?.toString() && route.query.type?.toString() !== '0' ? route.query.type?.toString() : undefined,
sort: ['type,asc', 'updateTime,desc'],
})
const paginationOption = reactive({
defaultPageSize: 30,
defaultSizeOptions: [30, 40, 50, 100, 120],
@@ -161,7 +173,7 @@ const handleClickFile = (item: FileItem) => {
},
}
filePreviewRef.value.onPreview({
fileInfo: { data: item.url, fileName: item.name, fileType: item.extension },
fileInfo: { data: item.url, fileName: item.originalName, fileType: item.extension },
excelConfig,
})
}
@@ -172,12 +184,21 @@ const handleClickFile = (item: FileItem) => {
previewFileAudioModal(item)
}
}
// 双击文件
const handleDblclickFile = (item: FileItem) => {
if (item.type === 0) {
queryForm.parentPath = `${item.parentPath === '/' ? '' : item.parentPath}/${item.name}`
search()
}
}
// 下载文件
const onDownload = async (fileInfo: FileItem) => {
const res = await downloadByUrl({
url: fileInfo.url,
target: '_self',
fileName: `${fileInfo.name}.${fileInfo.extension}`,
fileName: fileInfo.originalName,
})
res ? Message.success('下载成功') : Message.error('下载失败')
search()
@@ -188,11 +209,11 @@ const handleRightMenuClick = async (mode: string, fileInfo: FileItem) => {
if (mode === 'delete') {
Modal.warning({
title: '提示',
content: `是否确定删除文件${fileInfo.name}」?`,
content: `是否确定删除${fileInfo.type === 0 ? '文件夹' : '文件'}${fileInfo.originalName}」?`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onOk: async () => {
await deleteFile(fileInfo.id)
await deleteFile([fileInfo.id])
Message.success('删除成功')
search()
mittBus.emit('file-total-refresh')
@@ -223,6 +244,7 @@ const handleMulDelete = () => {
Message.success('删除成功')
search()
mittBus.emit('file-total-refresh')
isBatchMode.value = false
},
})
}
@@ -234,6 +256,7 @@ const handleUpload = (options: RequestOption) => {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append('parentPath', queryForm.parentPath ?? '/')
formData.append(name as string, fileItem.file as Blob)
try {
const res = await uploadFile(formData)
@@ -255,15 +278,51 @@ const handleUpload = (options: RequestOption) => {
onBeforeRouteUpdate((to) => {
if (!to.query.type) return
if (to.query.type === '0') {
if (to.query.type === '0' || !to.query.type) {
queryForm.type = undefined
queryForm.parentPath = '/'
} else {
queryForm.type = to.query.type?.toString()
queryForm.parentPath = undefined
}
search()
})
// 新建文件夹弹窗显示
const createDirModalVisible = ref<boolean>(false)
// 新文件名称
const newDirName = ref()
// 新建文件夹弹窗窗口取消事件
const handleCancel = () => {
newDirName.value = undefined
createDirModalVisible.value = false
}
// 新建文件夹弹窗窗口确认事件
const handleCreateDir = async () => {
await createDir(queryForm.parentPath ?? '/', newDirName.value)
newDirName.value = undefined
createDirModalVisible.value = false
search()
}
// 解析路径生成面包屑列表
const breadcrumbList = computed(() => {
const path = queryForm.parentPath || '/'
const parts = path.split('/').filter((p) => p !== '') // 分割路径并过滤空字符串
return parts.map((part, index) => {
const fullPath = parts.slice(0, index + 1).join('/')
return { name: part || '根目录', path: `/${fullPath}` }
})
})
// 处理面包屑点击
const handleBreadcrumbClick = (item) => {
queryForm.parentPath = item.path
search()
}
onMounted(() => {
search()
})
@@ -279,10 +338,30 @@ onMounted(() => {
overflow: hidden;
&__search {
border-bottom: 1px dashed var(--color-border-3);
margin: 16px $padding 0;
}
&__breadcrumb {
padding: 8px 16px;
background: var(--color-bg-2);
border-radius: 4px;
font-size: 14px;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-3);
:deep(.arco-breadcrumb-item) {
cursor: pointer;
}
:deep(.arco-breadcrumb-item-link) {
transition: color 0.2s;
&:hover {
color: var(--color-primary);
}
}
}
&__list {
flex: 1;
padding: 0 $padding $padding;

View File

@@ -0,0 +1,61 @@
<template>
<a-drawer v-model:visible="visible" title="公告详情" :width="width >= 500 ? 500 : '100%'" :footer="false">
<a-descriptions :column="2" size="large" class="general-description">
<a-descriptions-item label="ID" :span="2">
<a-typography-paragraph copyable>{{ dataDetail?.id }}</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="标题">{{ dataDetail?.title }}</a-descriptions-item>
<a-descriptions-item label="分类"><GiCellTag :value="dataDetail?.type" :dict="notice_type" /></a-descriptions-item>
<a-descriptions-item label="通知范围"><GiCellTag :value="dataDetail?.noticeScope" :dict="notice_scope_enum" /></a-descriptions-item>
<a-descriptions-item label="通知方式">
<span v-if="!dataDetail?.noticeMethods"></span>
<GiCellTags v-else :data="formatNoticeMethods(dataDetail?.noticeMethods)" />
</a-descriptions-item>
<a-descriptions-item label="是否定时">{{ dataDetail?.isTiming ? '是' : '否' }}</a-descriptions-item>
<a-descriptions-item label="发布时间">{{ dataDetail?.publishTime }}</a-descriptions-item>
<a-descriptions-item label="是否置顶">{{ dataDetail?.isTop ? '是' : '否' }}</a-descriptions-item>
<a-descriptions-item label="状态"><GiCellTag :value="dataDetail?.status" :dict="notice_status_enum" /></a-descriptions-item>
<a-descriptions-item label="创建人">{{ dataDetail?.createUserString }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ dataDetail?.createTime }}</a-descriptions-item>
<a-descriptions-item label="修改人">{{ dataDetail?.updateUserString }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ dataDetail?.updateTime }}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { type NoticeDetailResp, getNotice as getDetail } from '@/apis/system/notice'
import { useDict } from '@/hooks/app'
const { width } = useWindowSize()
const dataId = ref('')
const dataDetail = ref<NoticeDetailResp>()
const visible = ref(false)
const { notice_type, notice_scope_enum, notice_method_enum, notice_status_enum } = useDict('notice_type', 'notice_scope_enum', 'notice_method_enum', 'notice_status_enum')
// 格式化通知方式转换为GiCellTags所需格式
const formatNoticeMethods = (noticeMethods: string[]) => {
return noticeMethods.map((method) => {
const dictItem = notice_method_enum.value.find((item) => item.value === method)
return dictItem?.label || method
})
}
// 查询详情
const getDataDetail = async () => {
const { data } = await getDetail(dataId.value)
dataDetail.value = data
}
// 打开
const onOpen = async (id: string) => {
dataId.value = id
await getDataDetail()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss"></style>

View File

@@ -4,15 +4,27 @@
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" :subtitle="title" @back="onBack">
<template #extra>
<a-button type="primary" @click="save">
<template #icon>
<icon-save v-if="isUpdate" />
<icon-send v-else />
</template>
<template #default>
{{ isUpdate ? '保存' : '发布' }}
</template>
</a-button>
<a-space>
<a-button type="secondary" @click="onBack">
<template #icon>
<icon-close />
</template>
<template #default>取消</template>
</a-button>
<a-button v-if="!isUpdate || (isUpdate && form.status !== 3)" type="primary" status="warning" @click="save(true)">
<template #icon>
<icon-save />
</template>
<template #default>草稿</template>
</a-button>
<a-button type="primary" @click="save(false)">
<template #icon>
<icon-save v-if="isUpdate && form.status === 3" />
<icon-send v-else />
</template>
<template #default>{{ isUpdate && form.status === 3 ? '保存' : '发布' }}</template>
</a-button>
</a-space>
</template>
</a-page-header>
</a-affix>
@@ -35,6 +47,9 @@
</a-button>
</a-tooltip>
</template>
<template #noticeMethods>
<a-checkbox-group v-model="form.noticeMethods" :options="notice_method_enum" />
</template>
</GiForm>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
@@ -77,15 +92,18 @@ const isUpdate = computed(() => type === 'update')
const title = computed(() => (isUpdate.value ? '修改' : '新增'))
const containerRef = ref<HTMLElement | null>()
const formRef = ref<InstanceType<typeof GiForm>>()
const { notice_type } = useDict('notice_type')
const { notice_type, notice_scope_enum, notice_method_enum } = useDict('notice_type', 'notice_scope_enum', 'notice_method_enum')
const [form, resetForm] = useResetReactive({
title: '',
type: '',
effectiveTime: '',
terminateTime: '',
content: '',
noticeScope: 1,
noticeMethods: [1],
isTiming: false,
publishTime: undefined,
isTop: false,
status: 1,
})
const columns: ColumnItem[] = reactive([
@@ -93,6 +111,7 @@ const columns: ColumnItem[] = reactive([
label: '标题',
field: 'title',
type: 'input',
span: 24,
props: {
maxLength: 150,
showWordLimit: true,
@@ -100,36 +119,23 @@ const columns: ColumnItem[] = reactive([
rules: [{ required: true, message: '请输入标题' }],
},
{
label: '类',
label: '类',
field: 'type',
type: 'select',
props: {
options: notice_type,
},
rules: [{ required: true, message: '请输入类型' }],
},
{
label: '生效时间',
field: 'effectiveTime',
type: 'date-picker',
props: {
showTime: true,
},
},
{
label: '终止时间',
field: 'terminateTime',
type: 'date-picker',
props: {
showTime: true,
},
rules: [{ required: true, message: '请选择分类' }],
},
{
label: '通知范围',
field: 'noticeScope',
type: 'radio-group',
disabled: () => {
return form.status === 3
},
props: {
options: [{ label: '所有人', value: 1 }, { label: '指定用户', value: 2 }],
options: notice_scope_enum,
},
rules: [{ required: true, message: '请选择通知范围' }],
},
@@ -142,6 +148,54 @@ const columns: ColumnItem[] = reactive([
},
rules: [{ required: true, message: '请选择指定用户' }],
},
{
label: '通知方式',
field: 'noticeMethods',
type: 'checkbox',
disabled: () => {
return form.status === 3
},
},
{
label: '定时发布',
field: 'isTiming',
type: 'switch',
disabled: () => {
return form.status === 3
},
props: {
type: 'round',
checkedValue: true,
uncheckedValue: false,
checkedText: '是',
uncheckedText: '否',
},
},
{
label: '发布时间',
field: 'publishTime',
type: 'date-picker',
hide: () => {
return !form.isTiming
},
required: true,
props: {
showTime: true,
placeholder: '请选择发布时间',
},
},
{
label: '置顶',
field: 'isTop',
type: 'switch',
props: {
type: 'round',
checkedValue: true,
uncheckedValue: false,
checkedText: '是',
uncheckedText: '否',
},
},
])
// 修改
@@ -158,12 +212,13 @@ const onBack = () => {
}
// 保存
const save = async () => {
const save = async (isDraft: boolean) => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
try {
// 通知范围 所有人 去除指定用户
form.noticeUsers = form.noticeScope === 1 ? null : form.noticeUsers
form.status = isDraft ? 1 : 3
if (isUpdate.value) {
await updateNotice(form, id as string)
Message.success('修改成功')

View File

@@ -5,7 +5,7 @@
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['title']"
@@ -16,7 +16,7 @@
<a-select
v-model="queryForm.type"
:options="notice_type"
placeholder="请选择类"
placeholder="请选择类"
allow-clear
style="width: 150px"
@change="search"
@@ -32,25 +32,53 @@
<template #default>新增</template>
</a-button>
</template>
<template #noticeScope="{ record }">
<GiCellTag :value="record.noticeScope" :dict="notice_scope_enum" />
</template>
<template #noticeMethods="{ record }">
<span v-if="!record.noticeMethods"></span>
<GiCellTags v-else :data="formatNoticeMethods(record.noticeMethods)" />
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="notice_type" />
</template>
<template #status="{ record }">
<GiCellTag :value="record.status" :dict="notice_status_enum" />
</template>
<template #isTiming="{ record }">
<a-tag v-if="record.isTiming" color="arcoblue"></a-tag>
<a-tag v-else color="red"></a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:notice:get']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:notice:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link v-permission="['system:notice:delete']" status="danger" title="删除" @click="onDelete(record)"> 删除 </a-link>
<a-link v-permission="['system:notice:view']" title="预览" @click="onView(record)">查看</a-link>
<a-dropdown>
<a-button v-if="has.hasPermOr(['system:notice:update', 'system:notice:delete'])" type="text" size="mini" title="更多">
<template #icon>
<icon-more :size="16" />
</template>
</a-button>
<template #content>
<a-doption v-permission="['system:notice:update']">
<a-link title="修改" @click="onUpdate(record)">修改</a-link>
</a-doption>
<a-doption v-permission="['system:notice:delete']">
<a-link status="danger" title="删除" @click="onDelete(record)">删除</a-link>
</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</GiTable>
<NoticeDetailDrawer ref="NoticeDetailDrawerRef" />
</GiPageLayout>
</template>
<script setup lang="ts">
import type { TableInstance } from '@arco-design/web-vue'
import NoticeDetailDrawer from './NoticeDetailDrawer.vue'
import { type NoticeQuery, type NoticeResp, deleteNotice, listNotice } from '@/apis/system'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
@@ -59,7 +87,7 @@ import has from '@/utils/has'
defineOptions({ name: 'SystemNotice' })
const { notice_type, notice_status_enum } = useDict('notice_type', 'notice_status_enum')
const { notice_type, notice_scope_enum, notice_method_enum, notice_status_enum } = useDict('notice_type', 'notice_scope_enum', 'notice_method_enum', 'notice_status_enum')
const router = useRouter()
const queryForm = reactive<NoticeQuery>({
@@ -80,13 +108,15 @@ const columns: TableInstance['columns'] = [
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 200, ellipsis: true, tooltip: true },
{ title: '类型', dataIndex: 'type', slotName: 'type', align: 'center' },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '生效时间', dataIndex: 'effectiveTime', width: 180 },
{ title: '终止时间', dataIndex: 'terminateTime', width: 180 },
{ title: '创建人', dataIndex: 'createUserString', show: false, ellipsis: true, tooltip: true },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '公告标题', dataIndex: 'title', slotName: 'title', maxWidth: 180, ellipsis: true, tooltip: true },
{ title: '发布人', dataIndex: 'createUserString', maxWidth: 120, ellipsis: true, tooltip: true },
{ title: '通知范围', dataIndex: 'noticeScope', slotName: 'noticeScope', width: 110, align: 'center' },
{ title: '通知方式', dataIndex: 'noticeMethods', slotName: 'noticeMethods', maxWidth: 165, ellipsis: true, tooltip: true },
{ title: '分类', dataIndex: 'type', slotName: 'type', maxWidth: 100, align: 'center' },
{ title: '状态', dataIndex: 'status', slotName: 'status', maxWidth: 100, align: 'center' },
{ title: '是否定时', dataIndex: 'isTiming', slotName: 'isTiming', width: 110, align: 'center' },
{ title: '发布时间', dataIndex: 'publishTime', slotName: 'publishTime', width: 180 },
{ title: '是否置顶', dataIndex: 'isTop', slotName: 'isTop', show: false, maxWidth: 100, align: 'center' },
{
title: '操作',
dataIndex: 'action',
@@ -94,7 +124,7 @@ const columns: TableInstance['columns'] = [
width: 160,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:notice:get', 'system:notice:update', 'system:notice:delete']),
show: has.hasPermOr(['system:notice:get', 'system:notice:view', 'system:notice:update', 'system:notice:delete']),
},
]
@@ -123,9 +153,23 @@ const onUpdate = (record: NoticeResp) => {
router.push({ path: '/system/notice/add', query: { id: record.id, type: 'update' } })
}
const NoticeDetailDrawerRef = ref<InstanceType<typeof NoticeDetailDrawer>>()
// 详情
const onDetail = (record: NoticeResp) => {
router.push({ path: '/system/notice/detail', query: { id: record.id } })
NoticeDetailDrawerRef.value?.onOpen(record.id)
}
// 查看
const onView = (record: NoticeResp) => {
router.push({ path: '/system/notice/view', query: { id: record.id } })
}
// 格式化通知方式转换为GiCellTags所需格式
const formatNoticeMethods = (noticeMethods: string[]) => {
return noticeMethods.map((method) => {
const dictItem = notice_method_enum.value.find((item) => item.value === method)
return dictItem?.label || method
})
}
</script>

View File

@@ -7,31 +7,30 @@
</a-affix>
</div>
<div class="detail_content">
<h1 class="title">{{ form?.title }}</h1>
<h1 class="title">{{ dataDetail?.title }}</h1>
<div class="info">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ form?.createUserString }}</span>
<span>{{ dataDetail?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
}}</span>
<span>{{ dataDetail?.publishTime }}</span>
</span>
<a-divider v-if="form?.updateTime" direction="vertical" />
<span v-if="form?.updateTime">
<a-divider v-if="dataDetail?.updateTime" direction="vertical" />
<span v-if="dataDetail?.updateTime">
<icon-schedule class="icon" />
<span>更新时间</span>
<span>{{ form?.updateTime }}</span>
<span>{{ dataDetail?.updateTime }}</span>
</span>
</a-space>
</div>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
<AiEditor v-model="content" />
</div>
</div>
</div>
@@ -39,9 +38,8 @@
<script setup lang="ts">
import AiEditor from './components/index.vue'
import { getNotice } from '@/apis/system/notice'
import { type NoticeResp, getNotice } from '@/apis/system/notice'
import { useTabsStore } from '@/stores'
import { useResetReactive } from '@/hooks'
const route = useRoute()
const router = useRouter()
@@ -49,13 +47,8 @@ const tabsStore = useTabsStore()
const { id } = route.query
const containerRef = ref<HTMLElement | null>()
const [form, resetForm] = useResetReactive({
title: '',
createUserString: '',
effectiveTime: '',
createTime: '',
content: '',
})
const dataDetail = ref<NoticeResp>()
const content = computed(() => dataDetail.value?.content)
// 退
const onBack = () => {
@@ -65,9 +58,8 @@ const onBack = () => {
//
const onOpen = async (id: string) => {
resetForm()
const { data } = await getNotice(id)
Object.assign(form, data)
dataDetail.value = data
}
onMounted(() => {

View File

@@ -245,6 +245,9 @@ const select: TableInstance['onSelect'] = (rowKeys, checked, record) => {
const selectAll: TableInstance['onSelectAll'] = (checked) => {
tableData.value.forEach((item) => {
item.isChecked = checked
checked
? selectedKeys.value.add(item.id)
: selectedKeys.value.delete(item.id)
cascadeSelectChild(item, true)
})
}

View File

@@ -27,7 +27,7 @@
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-retry-button="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

View File

@@ -122,7 +122,7 @@ const queryFormColumns: ColumnItem[] = reactive([
span: { xs: 24, sm: 6, xxl: 8 },
props: {
options: DisEnableStatusList,
placeholder: '全部状态',
placeholder: '请选择状态',
},
},
{

View File

@@ -25,7 +25,7 @@
import { useWindowSize } from '@vueuse/core'
import { Message } from '@arco-design/web-vue'
import NProgress from 'nprogress'
import { type BehaviorCaptchaReq, getEmailCaptcha, updateUserEmail, updateUserPassword, updateUserPhone } from '@/apis'
import { type BehaviorCaptchaReq, getEmailCaptcha, getSmsCaptcha, updateUserEmail, updateUserPassword, updateUserPhone } from '@/apis'
import { encryptByRsa } from '@/utils/encrypt'
import { useUserStore } from '@/stores'
import { type ColumnItem, GiForm } from '@/components/GiForm'
@@ -159,6 +159,8 @@ const onCaptcha = async () => {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.formRef?.validateField(verifyType.value === 'phone' ? 'phone' : 'email')
if (isInvalid) return
// 重置行为参数
VerifyRef.value.instance.refresh()
VerifyRef.value.show()
}
@@ -188,15 +190,15 @@ const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
if (verifyType.value === 'phone') {
// await getSmsCaptcha(form.phone, captchaReq)
await getSmsCaptcha(form.phone, captchaReq)
} else if (verifyType.value === 'email') {
await getEmailCaptcha(form.email, captchaReq)
}
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
Message.success('发送成功')
// Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`

View File

@@ -7,7 +7,7 @@
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 800 }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
:disabled-tools="['size', 'setting', 'fullscreen']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
@select="select"
@select-all="selectAll"
@@ -15,9 +15,18 @@
>
<template #toolbar-left>
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
<a-select
v-model="queryForm.type"
placeholder="请选择类型"
allow-clear
style="width: 150px"
:options="message_type_enum"
@change="search"
>
</a-select>
<a-select
v-model="queryForm.isRead"
placeholder="全部状态"
placeholder="请选择状态"
allow-clear
style="width: 150px"
@change="search"
@@ -36,37 +45,79 @@
删除
</a-button>
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onRead">
标记已读
标记已读
</a-button>
<a-button type="primary" :disabled="selectedKeys.length > 0" :title="!selectedKeys.length ? '请选择' : ''" @click="onReadAll">
全部已读
</a-button>
</template>
<template #title="{ record }">
<a-tooltip :content="record.content"><span>{{ record.title }}</span></a-tooltip>
<a-link @click="showMessageDetail(record)">
<a-typography-paragraph
class="link-text"
:ellipsis="{
rows: 1,
showTooltip: true,
css: true,
}"
>
{{ record.title }}
</a-typography-paragraph>
</a-link>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="message_type_enum" />
</template>
<template #isRead="{ record }">
<a-tag :color="record.isRead ? '' : 'arcoblue'">
{{ record.isRead ? '已读' : '未读' }}
</a-tag>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="message_type" />
</template>
</GiTable>
<!-- 消息详情弹窗 -->
<a-modal
v-model:visible="messageDetailVisible"
:width="width >= 500 ? 500 : '100%'"
:footer="false"
:mask-closable="true"
class="message-detail-modal"
>
<template #title>{{ currentMessage?.title }}</template>
<div class="message-detail-content">
<div class="message-content">{{ currentMessage?.content }}</div>
<div class="message-footer">
<div class="time-info">
<icon-clock-circle class="time-icon" />
<span class="time-label">发送时间</span>
<span class="time-value">{{ currentMessage?.createTime }}</span>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import type { TableInstance } from '@arco-design/web-vue'
import { Message, Modal } from '@arco-design/web-vue'
import { type MessageQuery, deleteMessage, listMessage, readAllMessage, readMessage } from '@/apis'
import { useWindowSize } from '@vueuse/core'
import {
type MessageQuery,
type MessageResp,
deleteMessage,
getUserMessage,
listMessage,
readAllMessage,
readMessage,
} from '@/apis'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import mittBus from '@/utils/mitt'
defineOptions({ name: 'SystemMessage' })
defineOptions({ name: 'UserMyMessage' })
const { message_type } = useDict('message_type')
const { width } = useWindowSize()
const { message_type_enum } = useDict('message_type_enum')
const queryForm = reactive<MessageQuery>({
sort: ['createTime,desc'],
@@ -94,9 +145,9 @@ const columns: TableInstance['columns'] = [
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 100, ellipsis: true, tooltip: true },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', minWidth: 100, align: 'center' },
{ title: '时间', dataIndex: 'createTime', width: 180 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
]
// 重置
@@ -139,6 +190,98 @@ const onReadAll = async () => {
},
})
}
const messageDetailVisible = ref(false)
const currentMessage = ref<MessageResp>()
// 显示消息详情
const showMessageDetail = async (record: any) => {
messageDetailVisible.value = true
const { data } = await getUserMessage(record.id)
currentMessage.value = data
record.isRead = currentMessage.value?.isRead
onSuccess()
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.link-text {
margin-bottom: 0;
font-weight: 500;
:deep(.arco-typography) {
margin-bottom: 0;
}
}
:deep(.message-detail-modal) {
.arco-modal-header {
border-bottom: 1px solid var(--color-border-2);
padding: 20px 24px 16px;
.arco-modal-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
}
.arco-modal-body {
padding: 24px;
}
}
.message-detail-content {
.message-content {
margin-bottom: 24px;
line-height: 1.6;
color: var(--color-text-1);
font-size: 14px;
min-height: 80px;
word-wrap: break-word;
white-space: pre-wrap;
}
.message-footer {
.time-info {
display: flex;
align-items: center;
gap: 6px;
.time-icon {
font-size: 14px;
color: var(--color-text-3);
}
.time-label {
font-size: 13px;
color: var(--color-text-3);
}
.time-value {
font-size: 13px;
color: var(--color-text-2);
font-weight: 500;
}
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid var(--color-border-2);
background-color: var(--color-bg-1);
.arco-btn {
display: flex;
align-items: center;
gap: 4px;
.btn-icon {
font-size: 14px;
}
}
}
</style>

View File

@@ -14,7 +14,7 @@
<a-select
v-model="queryForm.type"
:options="notice_type"
placeholder="全部类型"
placeholder="请选择类型"
allow-clear
style="width: 150px"
@change="search"
@@ -25,7 +25,7 @@
</a-button>
</template>
<template #title="{ record }">
<a-link @click="onDetail(record)">
<a-link @click="onView(record)">
<a-typography-paragraph
class="link-text"
:ellipsis="{
@@ -41,6 +41,11 @@
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="notice_type" />
</template>
<template #isRead="{ record }">
<a-tag :color="record.isRead ? '' : 'arcoblue'">
{{ record.isRead ? '已读' : '未读' }}
</a-tag>
</template>
</GiTable>
</template>
@@ -50,7 +55,7 @@ import { type NoticeQuery, type NoticeResp, listUserNotice } from '@/apis/system
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'SystemMessage' })
defineOptions({ name: 'UserMyNotice' })
const { notice_type } = useDict('notice_type')
@@ -72,10 +77,11 @@ const columns: TableInstance['columns'] = [
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', ellipsis: true, tooltip: true },
{ title: '类', dataIndex: 'type', slotName: 'type', align: 'center' },
{ title: '公告标题', dataIndex: 'title', slotName: 'title', ellipsis: true, tooltip: true },
{ title: '类', dataIndex: 'type', slotName: 'type', align: 'center' },
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', align: 'center' },
{ title: '发布人', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
{ title: '发布时间', dataIndex: 'createTime', width: 180 },
{ title: '发布时间', dataIndex: 'publishTime', width: 180 },
]
// 重置
@@ -86,8 +92,8 @@ const reset = () => {
}
const router = useRouter()
// 详情
const onDetail = (record: NoticeResp) => {
// 查看
const onView = (record: NoticeResp) => {
router.push({ path: '/user/notice', query: { id: record.id } })
}
</script>

View File

@@ -0,0 +1,324 @@
<template>
<a-modal
v-model:visible="visible"
:title="currentNotice?.title || '系统公告'"
:width="800"
:mask-closable="false"
:footer="false"
@cancel="onClose"
>
<div class="detail">
<div class="detail_content">
<h1 class="title">{{ currentNotice?.title }}</h1>
<div class="info">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ currentNotice?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ currentNotice?.publishTime }}</span>
</span>
<a-divider v-if="currentNotice?.updateTime" direction="vertical" />
<span v-if="currentNotice?.updateTime">
<icon-schedule class="icon" />
<span>更新时间</span>
<span>{{ currentNotice?.updateTime }}</span>
</span>
</a-space>
</div>
<div style="flex: 1;">
<div v-if="contentLoading" class="content-loading">
<a-spin size="large" />
</div>
<AiEditor v-else v-model:model-value="currentNoticeContent" />
</div>
</div>
<!-- 底部操作区域 -->
<div class="notice-footer">
<div class="notice-actions">
<span class="pagination-info">
{{ currentIndex + 1 }} / {{ unreadNoticeIds.length }}
</span>
<!-- 翻页按钮 -->
<div class="pagination-controls">
<a-button
v-if="currentIndex > 0"
@click="previousNotice"
>
上一篇
</a-button>
<a-button
v-if="currentIndex < unreadNoticeIds.length - 1"
@click="nextNotice"
>
下一篇
</a-button>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import AiEditor from './view/components/index.vue'
import { getUnreadNoticeIds, getUserNotice } from '@/apis/system/user-message'
import type { NoticePreviewResp } from '@/apis/system'
defineOptions({ name: 'NoticePopup' })
const props = withDefaults(defineProps<Props>(), {
method: 'POPUP',
})
const emit = defineEmits<{
close: []
}>()
interface Props {
method?: string // 通知方式,默认为 'POPUP'
}
const visible = ref(false)
const unreadNoticeIds = ref<number[]>([])
const currentIndex = ref(0)
const loading = ref(false)
const contentLoading = ref(false)
const noticeCache = ref<Map<number, NoticePreviewResp>>(new Map())
const currentNotice = computed(() => {
const noticeId = unreadNoticeIds.value[currentIndex.value]
return noticeId ? noticeCache.value.get(noticeId) : null
})
const currentNoticeContent = computed(() => {
return currentNotice.value?.content || ''
})
// 获取未读公告ID列表
const fetchNoticeDetail = async (index: number) => {
const noticeId = unreadNoticeIds.value[index]
if (!noticeId) {
return
}
// 如果已经缓存了该公告,直接设置当前索引并返回
if (noticeCache.value.has(noticeId)) {
currentIndex.value = index
return
}
try {
contentLoading.value = true
const { data } = await getUserNotice(noticeId)
noticeCache.value.set(noticeId, data as NoticePreviewResp)
// 确保设置当前索引,触发计算属性更新
currentIndex.value = index
} catch (error) {
console.error(`获取公告详情失败:`, error)
// 创建一个错误状态的公告对象
noticeCache.value.set(noticeId, {
id: noticeId,
title: '获取公告失败',
content: '获取公告内容失败,请稍后重试',
createUserString: '',
publishTime: '',
} as NoticePreviewResp)
// 即使出错也要设置当前索引
currentIndex.value = index
} finally {
contentLoading.value = false
}
}
// 获取指定公告的详情
const fetchUnreadNotices = async () => {
try {
loading.value = true
const { data: noticeIds } = await getUnreadNoticeIds(props.method)
if (noticeIds && noticeIds.length > 0) {
unreadNoticeIds.value = noticeIds
visible.value = true
// 获取第一篇公告的详情
await fetchNoticeDetail(0)
// 确保当前索引设置为0
currentIndex.value = 0
}
} catch (error) {
console.error('获取未读公告失败:', error)
} finally {
loading.value = false
}
}
// 上一条公告
const previousNotice = async () => {
if (currentIndex.value > 0) {
// 计算新的索引
const newIndex = currentIndex.value - 1
// 获取公告详情
await fetchNoticeDetail(newIndex)
}
}
// 下一条公告
const nextNotice = async () => {
if (currentIndex.value < unreadNoticeIds.value.length - 1) {
// 计算新的索引
const newIndex = currentIndex.value + 1
// 获取公告详情
await fetchNoticeDetail(newIndex)
}
}
// 关闭弹窗
const onClose = () => {
visible.value = false
currentIndex.value = 0
unreadNoticeIds.value = []
noticeCache.value.clear()
emit('close')
}
// 打开弹窗
const open = () => {
fetchUnreadNotices()
}
defineExpose({
open,
})
</script>
<style scoped lang="scss">
.detail {
.detail_content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0; // 减小内边距
margin: 0;
.title {
margin-bottom: 12px; // 减小标题下边距
color: var(--color-text-1);
line-height: 1.4;
text-align: center;
}
.info {
margin-bottom: 12px; // 减小信息区域下边距
color: var(--color-text-2);
font-size: 14px;
line-height: 1.5715;
text-align: center;
.icon {
margin-right: 4px;
}
}
}
.notice-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-border-2);
.notice-actions {
display: flex;
justify-content: space-between;
align-items: center;
.pagination-info {
font-size: 14px;
color: var(--color-text-2);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 12px;
}
}
}
}
.content-loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
// 兼容原有样式
.notice-content {
.notice-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border-2);
.notice-meta {
display: flex;
align-items: center;
gap: 12px;
.notice-time {
font-size: 12px;
color: var(--color-text-3);
}
}
}
.notice-body {
margin-bottom: 24px;
.notice-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
margin-bottom: 16px;
line-height: 1.4;
}
.notice-text {
font-size: 14px;
color: var(--color-text-2);
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
:deep(img) {
max-width: 100%;
height: auto;
}
:deep(p) {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
.notice-loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
</style>

View File

@@ -45,7 +45,7 @@ watch(() => props.modelValue, (value) => {
editorConfig.content = value
init()
}
})
}, { deep: true })
watch(() => appStore.theme, (value) => {
editorConfig.theme = value
init()
@@ -54,6 +54,7 @@ watch(() => appStore.theme, (value) => {
//
onMounted(() => {
editorConfig.element = divRef.value
editorConfig.content = props.modelValue
init()
})
//

View File

@@ -19,8 +19,7 @@
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
}}</span>
<span>{{ form?.publishTime }}</span>
</span>
<a-divider v-if="form?.updateTime" direction="vertical" />
<span v-if="form?.updateTime">
@@ -54,8 +53,7 @@ const containerRef = ref<HTMLElement | null>()
const [form, resetForm] = useResetReactive({
title: '',
createUserString: '',
effectiveTime: '',
createTime: '',
publishTime: '',
content: '',
})

View File

@@ -18,9 +18,7 @@
</a-tab-pane>
</a-tabs>
</template>
<transition name="fade-slide" mode="out-in" appear>
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
</transition>
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
</GiPageLayout>
</template>
@@ -29,7 +27,10 @@ import { useRoute, useRouter } from 'vue-router'
import MyMessage from './components/MyMessage.vue'
import MyNotice from './components/MyNotice.vue'
import { useDevice } from '@/hooks'
import { type MessageResp, type NoticeResp, listMessage, listNotice } from '@/apis'
import {
getUnreadMessageCount,
getUnreadNoticeCount,
} from '@/apis'
import mittBus from '@/utils/mitt'
defineOptions({ name: 'UserMessage' })
@@ -51,35 +52,22 @@ const TabPaneTitle = defineComponent({
const { isDesktop } = useDevice()
const messageList = ref<MessageResp[]>()
const noticeList = ref<NoticeResp[]>()
const unreadMessageCount = ref(0)
const unreadNoticeCount = ref(0)
const tabItems = computed(() => [
{ key: 'msg', title: '我的消息', count: messageList.value?.length ?? 0 },
{ key: 'notice', title: '我的公告' },
{ key: 'msg', title: '我的消息', count: unreadMessageCount.value },
{ key: 'notice', title: '我的公告', count: unreadNoticeCount.value },
])
const messageQueryParam = reactive({
isRead: false,
sort: ['createTime,desc'],
page: 1,
size: 5,
})
const noticeQueryParam = reactive({
sort: ['createTime,desc'],
page: 1,
size: 5,
})
const getMessageData = async () => {
const { data } = await listMessage(messageQueryParam)
messageList.value = data.list.filter((item) => !item.isRead)
const { data } = await getUnreadMessageCount()
unreadMessageCount.value = data.total
}
const getNoticeData = async () => {
const { data } = await listNotice(noticeQueryParam)
noticeList.value = data.list
const { data } = await getUnreadNoticeCount()
unreadNoticeCount.value = data.total
}
onMounted(() => {
@@ -87,6 +75,7 @@ onMounted(() => {
getNoticeData()
mittBus.on('count-refresh', () => {
getMessageData()
getNoticeData()
})
})