mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2026-01-02 06:58:37 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9199f06f6 | |||
| a6d6bdb742 | |||
| fa1291bda2 | |||
| 8c100e5753 | |||
| 47f4ca611e | |||
| 1ef1ba6ec8 | |||
|
|
bce9fa6938 | ||
| a239dbd1ea | |||
|
|
aa14c41df0 | ||
|
|
f66f80fc56 | ||
| 9faee319dd | |||
| e2d436fb30 | |||
| 672f93c52e | |||
| abf3f13041 | |||
| feef35f541 | |||
| 04444a4bd8 | |||
| 2018cf0ead | |||
| 5511c87773 | |||
| abdd773886 | |||
|
|
86fb09efaa | ||
| b680ee3fac | |||
| dc66e9e62c | |||
|
|
1940f6aaa1 | ||
| 70e3b6dace | |||
| 5c689678da | |||
| b05ec99d35 | |||
| c9fe54c2d6 | |||
| 5768d55654 | |||
| 4e167368c4 | |||
|
|
a8986b93a8 | ||
|
|
55ce849b2e | ||
|
|
8c700990a0 | ||
| dd1504204c | |||
|
|
70e2de3250 | ||
| 471f30e1e7 | |||
| d927d8f58a |
@@ -17,5 +17,5 @@ VITE_OPEN_DEVTOOLS = false
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = true
|
||||
|
||||
# 终端ID
|
||||
# 客户端ID
|
||||
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
||||
@@ -13,5 +13,5 @@ VITE_BASE = '/'
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = true
|
||||
|
||||
# 终端ID
|
||||
# 客户端ID
|
||||
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
||||
@@ -18,5 +18,5 @@ VITE_OPEN_DEVTOOLS = true
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = false
|
||||
|
||||
# 终端ID
|
||||
# 客户端ID
|
||||
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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*
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -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))
|
||||
|
||||
@@ -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 有效期
|
||||
- 在线用户:管理当前登录用户,可一键踢除下线
|
||||
- 日志管理:管理系统登录日志、操作日志,支持查看日志详情,包含请求头、响应头等报文信息
|
||||
- 短信日志:管理系统短信发送日志,支持删除、导出
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
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
@@ -11,6 +11,7 @@ export interface DashboardNoticeResp {
|
||||
id: number
|
||||
title: string
|
||||
type: number
|
||||
isTop: boolean
|
||||
}
|
||||
|
||||
/** 仪表盘访问趋势类型 */
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface JobResp {
|
||||
jobName: string
|
||||
description?: string
|
||||
triggerType: number
|
||||
triggerInterval: string | number
|
||||
triggerInterval: string
|
||||
executorType: number
|
||||
taskType: number
|
||||
executorInfo: string
|
||||
|
||||
@@ -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] })
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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 新增公告 */
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
src/assets/icons/swagger.svg
Normal file
1
src/assets/icons/swagger.svg
Normal 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 |
@@ -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">
|
||||
|
||||
@@ -30,4 +30,8 @@ interface Props {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
:deep(.arco-overflow-list-overflow) {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))',
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '禁用',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: '禁用',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -101,7 +101,7 @@ const columns: ColumnItem[] = reactive([
|
||||
field: 'domain',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
required: true,
|
||||
required: false,
|
||||
show: () => form.type === 2,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 || ''
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
61
src/views/system/notice/NoticeDetailDrawer.vue
Normal file
61
src/views/system/notice/NoticeDetailDrawer.vue
Normal 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>
|
||||
@@ -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('修改成功')
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -122,7 +122,7 @@ const queryFormColumns: ColumnItem[] = reactive([
|
||||
span: { xs: 24, sm: 6, xxl: 8 },
|
||||
props: {
|
||||
options: DisEnableStatusList,
|
||||
placeholder: '全部状态',
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
324
src/views/user/message/components/NoticePopup.vue
Normal file
324
src/views/user/message/components/NoticePopup.vue
Normal 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>
|
||||
@@ -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()
|
||||
})
|
||||
// 销毁阶段
|
||||
@@ -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: '',
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user