This commit is contained in:
Truong Nguyen 2024-07-10 18:08:06 +07:00
parent 5f6e80bbe8
commit f0c4f1f39e
86 changed files with 3228 additions and 624 deletions

4
.env
View File

@ -1,5 +1,5 @@
# 平台本地运行端口号
# Cổng chạy cục bộ nền tảng
VITE_PORT = 8848
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
# Có ẩn trang chủ hay không: ẩn true, không ẩn false (Đừng xóa, VITE_HIDE_HOME chỉ cần cấu hình trong tệp .env)
VITE_HIDE_HOME = false

View File

@ -1,8 +1,8 @@
# 平台本地运行端口号
# Cổng chạy cục bộ nền tảng
VITE_PORT = 8848
# 开发环境读取配置文件路径
# Đường dẫn đọc tệp cấu hình môi trường phát triển
VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
# Chế độ lịch sử của router môi trường phát triển (Chế độ Hash truyền "hash", Chế độ HTML5 truyền "h5", Chế độ Hash với tham số base truyền "hash,base", Chế độ HTML5 với tham số base truyền "h5,base")
VITE_ROUTER_HISTORY = "hash"

View File

@ -1,13 +1,13 @@
# 线上环境平台打包路径
# Đường dẫn đóng gói nền tảng môi trường trực tuyến
VITE_PUBLIC_PATH = /
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
# Chế độ lịch sử của router môi trường trực tuyến (Chế độ Hash truyền "hash", Chế độ HTML5 truyền "h5", Chế độ Hash với tham số base truyền "hash,base", Chế độ HTML5 với tham số base truyền "h5,base")
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
# Có sử dụng CDN để thay thế thư viện địa phương khi đóng gói hay không: thay thế true, không thay thế false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"
# Có bật nén gzip hoặc nén brotli hay không (chia thành hai trường hợp, xóa tệp gốc và không xóa tệp gốc)
# Cấu hình không xóa tệp gốc khi nén: gzip, brotli, both (bật đồng thời nén gzip và brotli), none (không bật nén, mặc định)
# Cấu hình xóa tệp gốc khi nén: gzip-clear, brotli-clear, both-clear (bật đồng thời nén gzip và brotli), none (không bật nén, mặc định)
VITE_COMPRESSION = "none"

View File

@ -1,16 +1,16 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
# Pre-release cũng cần hành vi của môi trường sản xuất
# https://vitejs.dev/guide/env-and-mode.html#modes
# NODE_ENV = development
VITE_PUBLIC_PATH = /
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
# Chế độ lịch sử của router môi trường pre-release (Chế độ Hash truyền "hash", Chế độ HTML5 truyền "h5", Chế độ Hash với tham số base truyền "hash,base", Chế độ HTML5 với tham số base truyền "h5,base")
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
# Có sử dụng CDN để thay thế thư viện địa phương khi đóng gói hay không: thay thế true, không thay thế false
VITE_CDN = true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# Có bật nén gzip hoặc nén brotli hay không (chia thành hai trường hợp, xóa tệp gốc và không xóa tệp gốc)
# Cấu hình không xóa tệp gốc khi nén: gzip, brotli, both (bật đồng thời nén gzip và brotli), none (không bật nén, mặc định)
# Cấu hình xóa tệp gốc khi nén: gzip-clear, brotli-clear, both-clear (bật đồng thời nén gzip và brotli), none (không bật nén, mặc định)
VITE_COMPRESSION = "none"

View File

@ -36,7 +36,7 @@
"js"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.displayLanguage": "vi",
"i18n-ally.enabledFrameworks": [
"vue"
],

View File

@ -2,7 +2,7 @@
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**English** | [中文](./README.md)
**English** | [TIẾNG VIỆT](./README.md)
## Introduce

View File

@ -1,51 +1,51 @@
<h1>vue-pure-admin精简版(国际化版本)</h1>
<h1>vue-pure-admin phiên bản tinh gọn (phiên bản quốc tế hóa)</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md)
## 介绍
## Giới thiệu
精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
Phiên bản tinh gọn được trích xuất từ [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), chứa các chức năng chính, phù hợp hơn cho việc phát triển dự án thực tế. Kích thước gói sau khi đóng gói, khi nhập toàn cục [element-plus](https://element-plus.org) vẫn dưới `2.3MB` và sẽ luôn đồng bộ mã với phiên bản đầy đủ. Khi bật nén `brotli` và thay thế thư viện địa phương bằng `cdn`, kích thước gói đóng gói sẽ dưới `350kb`.
## 版本选择
## Lựa chọn phiên bản
当前是国际化版本,如果您需要非国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin)
Hiện tại là phiên bản quốc tế hóa, nếu bạn cần phiên bản không quốc tế hóa [hãy nhấp vào đây](https://github.com/pure-admin/pure-admin-thin).
## `js` 版本
## Phiên bản `js`
[点我查看 js 版本](https://pure-admin.github.io/pure-admin-doc/pages/js/)
[Nhấp vào đây để xem phiên bản js](https://pure-admin.github.io/pure-admin-doc/pages/js/)
## `max` 版本
## Phiên bản `max`
[点我查看 max 版本](https://github.com/pure-admin/vue-pure-admin-max)
[Nhấp vào đây để xem phiên bản max](https://github.com/pure-admin/vue-pure-admin-max)
## 配套视频
## Video hướng dẫn kèm theo
[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
[Nhấp vào đây để xem thiết kế UI](https://www.bilibili.com/video/BV17g411T7rq)
[Nhấp vào đây để xem hướng dẫn phát triển nhanh](https://www.bilibili.com/video/BV1kg411v7QT)
## 配套保姆级文档
## Tài liệu hướng dẫn kèm theo
[点我查看 vue-pure-admin 文档](https://pure-admin.github.io/pure-admin-doc)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
[Nhấp vào đây để xem tài liệu vue-pure-admin](https://pure-admin.github.io/pure-admin-doc)
[Nhấp vào đây để xem tài liệu @pureadmin/utils](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
## Dịch vụ chất lượng, gia công phần mềm, hỗ trợ tài trợ
[点我查看详情](https://pure-admin.github.io/pure-admin-doc/pages/service/)
[Nhấp vào đây để xem chi tiết](https://pure-admin.github.io/pure-admin-doc/pages/service/)
## 预览
## Xem trước
[查看预览](https://pure-admin-thin.netlify.app/#/login)
[Xem trước](https://pure-admin-thin.netlify.app/#/login)
## 维护者
## Người bảo trì
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ 注意
## ⚠️ Chú ý
精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
Phiên bản tinh gọn không chấp nhận bất kỳ `issues``pr`, nếu có vấn đề xin vui lòng đến phiên bản đầy đủ [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) để đề xuất, cảm ơn!
## 许可证
## Giấy phép
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -1,4 +1,5 @@
buttons:
pureAccountSettings: Account
pureLoginOut: LoginOut
pureLogin: Login
pureOpenSystemSet: Open System Configs
@ -63,13 +64,124 @@ panel:
menus:
pureHome: Home
pureLogin: Login
pureEmpty: Empty Page
pureTable: Table
pureSysManagement: System Manage
pureUser: User Manage
pureRole: Role Manage
pureSystemMenu: Menu Manage
pureDept: Dept Manage
pureSysMonitor: System Monitor
pureOnlineUser: Online User
pureLoginLog: Login Log
pureOperationLog: Operation Log
pureSystemLog: System Log
pureEditor: Editor
pureAbnormal: Abnormal Page
pureFourZeroFour: "404"
pureFourZeroOne: "403"
pureFive: "500"
pureComponents: Components
pureDialog: Dialog
pureMessage: Message Tips
pureVideo: Video
pureSegmented: Segmented
pureWaterfall: Waterfall
pureMap: Map
pureDraggable: Draggable
pureSplitPane: Split Pane
pureText: Text Ellipsis
pureElButton: Button
pureButton: Button Animation
pureCheckButton: Check Button
pureCropping: Picture Cropping
pureAnimatecss: AnimateCss Selector
pureCountTo: Digital Animation
pureSelector: Scope Selector
pureFlowChart: Flow Chart
pureSeamless: Seamless Scroll
pureContextmenu: Context Menu
pureTypeit: Typeit
pureJsonEditor: JSON Editor
pureColorPicker: Color Picker
pureDatePicker: Date Picker
pureDateTimePicker: DateTimePicker
pureTimePicker: TimePicker
pureTag: Tag
pureStatistic: Statistic
pureCollapse: Collapse
pureGanttastic: Gantt Chart
pureProgress: Progress
pureUpload: File Upload
pureCheckCard: CheckCard
pureMenus: MultiLevel Menu
pureMenu1: Menu1
pureMenu1-1: Menu1-1
pureMenu1-2: Menu1-2
pureMenu1-2-1: Menu1-2-1
pureMenu1-2-2: Menu1-2-2
pureMenu1-3: Menu1-3
pureMenu2: Menu2
purePermission: Permission Manage
purePermissionPage: Page Permission
purePermissionButton: Button Permission
pureTabs: Tabs Operate
pureGuide: Guide
pureAble: Able
pureMenuTree: Menu Tree
pureVideoFrame: Video Frame Capture
pureWavesurfer: Audio Visualization
pureRipple: Ripple
pureMqtt: Mqtt Client
pureOptimize: Debounce、Throttle、Copy、Longpress Directives
pureVerify: Captcha
pureWatermark: Water Mark
purePrint: Print
pureDownload: Download
pureExternalPage: External Page
pureExternalDoc: Docs External
pureEmbeddedDoc: Docs Embedded
pureExternalLink: Vue-Pure-Admin
pureUtilsLink: Pure-Admin-Utils
pureColorHuntDoc: ColorHunt
pureUiGradients: UiGradients
pureEpDoc: Element-Plus
pureTailwindcssDoc: Tailwindcss
pureVueDoc: Vue3
pureViteDoc: Vite
purePiniaDoc: Pinia
pureRouterDoc: Vue-Router
pureAbout: About
pureResult: Result Page
pureSuccess: Success Page
pureFail: Fail Page
pureIconSelect: Icon Select
pureTimeline: Time Line
pureLineTree: LineTree
pureList: List Page
pureCardList: Card List Page
pureDebounce: Debounce & Throttle
pureFormDesign: Form Design
pureBarcode: Barcode
pureQrcode: Qrcode
pureCascader: Area Cascader
pureSwiper: Swiper Plugin
pureVirtualList: Virtual List
purePdf: PDF Preview
pureExcel: Export Excel
pureInfiniteScroll: Table Infinite Scroll
pureSensitive: Sensitive Filter
purePinyin: PinYin
pureDanmaku: Danmaku
pureSchemaForm: Form
pureTableBase: Base Usage
pureTableHigh: High Usage
pureTableEdit: Edit Usage
pureVxeTable: Virtual Usage
pureBoard: Paint Board
pureMindMap: Mind Map
pureMenuOverflow: Menu Overflow Show Tooltip Text
pureChildMenuOverflow: Child Menu Overflow Show Tooltip Text
status:
pureLoad: Loading...
pureMessage: Message
@ -81,9 +193,42 @@ status:
login:
pureUsername: Username
purePassword: Password
pureVerifyCode: VerifyCode
pureRemember: days no need to login
pureRememberInfo: After checking and logging in, will automatically log in to the system without entering your username and password within the specified number of days.
pureSure: Sure Password
pureForget: Forget Password?
pureLogin: Login
pureThirdLogin: Third Login
purePhoneLogin: Phone Login
pureQRCodeLogin: QRCode Login
pureRegister: Register
pureWeChatLogin: WeChat Login
pureAlipayLogin: Alipay Login
pureQQLogin: QQ Login
pureWeiBoLogin: Weibo Login
purePhone: Phone
pureSmsVerifyCode: SMS VerifyCode
pureBack: Back
pureTest: Mock Test
pureTip: After scanning the code, click "Confirm" to complete the login
pureDefinite: Definite
pureLoginSuccess: Login Success
pureLoginFail: Login Fail
pureRegisterSuccess: Regist Success
pureTickPrivacy: Please tick Privacy Policy
pureReadAccept: I have read it carefully and accept
purePrivacyPolicy: Privacy Policy
pureGetVerifyCode: Get VerifyCode
pureInfo: Seconds
pureUsernameReg: Please enter username
purePassWordReg: Please enter password
purePassWordRuleReg: The password format should be any combination of 8-18 digits
pureVerifyCodeReg: Please enter verify code
pureVerifyCodeCorrectReg: Please enter correct verify code
pureVerifyCodeSixReg: Please enter a 6-digit verify code
purePhoneReg: Please enter the phone
purePhoneCorrectReg: Please enter the correct phone number format
purePassWordRuleReg: The password format should be any combination of 8-18 digits
purePassWordSureReg: Please enter confirm password
purePassWordDifferentReg: The two passwords do not match!
purePassWordUpdateReg: Password has been updated

238
locales/vi.yaml Normal file
View File

@ -0,0 +1,238 @@
buttons:
pureAccountSettings: Tài khoản
pureBackTop: Về đầu trang
pureClickCollapse: Thu gọn
pureClickExpand: Mở rộng
pureClose: Đóng
pureCloseAllTabs: Đóng tất cả các tab
pureCloseCurrentTab: Đóng tab hiện tại
pureCloseLeftTabs: Đóng các tab bên trái
pureCloseOtherTabs: Đóng các tab khác
pureCloseRightTabs: Đóng các tab bên phải
pureCloseText: Đóng
pureConfirm: Xác nhận
pureContentExitFullScreen: Thoát toàn màn hình nội dung
pureContentFullScreen: Toàn màn hình nội dung
pureLogin: Đăng nhập
pureLoginOut: Đăng xuất
pureOpenSystemSet: Mở Cấu hình Hệ thống
pureOpenText: Mở
pureReload: Tải lại
pureSwitch: Chuyển đổi
login:
pureAlipayLogin: Đăng nhập bằng Alipay
pureBack: Quay lại
pureDefinite: Xác định
pureForget: Quên mật khẩu?
pureGetVerifyCode: Lấy mã xác nhận
pureInfo: Giây
pureLogin: Đăng nhập
pureLoginFail: Đăng nhập thất bại
pureLoginSuccess: Đăng nhập thành công
purePassWordDifferentReg: Hai mật khẩu không khớp nhau!
purePassWordReg: Vui lòng nhập mật khẩu
purePassWordRuleReg: Định dạng mật khẩu phải là bất kỳ kết hợp nào từ 8-18 chữ số
purePassWordSureReg: Vui lòng nhập lại mật khẩu
purePassWordUpdateReg: Mật khẩu đã được cập nhật
purePassword: Mật khẩu
purePhone: Điện thoại
purePhoneCorrectReg: Vui lòng nhập đúng định dạng số điện thoại
purePhoneLogin: Đăng nhập bằng điện thoại
purePhoneReg: Vui lòng nhập số điện thoại
purePrivacyPolicy: Chính sách bảo mật
pureQQLogin: Đăng nhập bằng QQ
pureQRCodeLogin: Đăng nhập bằng mã QR
pureReadAccept: Tôi đã đọc và chấp nhận kỹ
pureRegister: Đăng ký
pureRegisterSuccess: Đăng ký thành công
pureRemember: ngày không cần đăng nhập
pureRememberInfo: >-
Sau khi chọn và đăng nhập, hệ thống sẽ tự động đăng nhập mà không cần nhập
lại tên người dùng và mật khẩu của bạn trong số ngày được chỉ định.
pureSmsVerifyCode: Mã xác nhận SMS
pureSure: Xác nhận mật khẩu
pureTest: Kiểm tra giả
pureThirdLogin: Đăng nhập bằng bên thứ ba
pureTickPrivacy: Vui lòng đánh dấu Chính sách bảo mật
pureTip: Sau khi quét mã, nhấn "Xác nhận" để hoàn thành đăng nhập
pureUsername: Tên người dùng
pureUsernameReg: Vui lòng nhập tên người dùng
pureVerifyCode: Mã xác nhận
pureVerifyCodeCorrectReg: Vui lòng nhập đúng mã xác nhận
pureVerifyCodeReg: Vui lòng nhập mã xác nhận
pureVerifyCodeSixReg: Vui lòng nhập mã xác nhận 6 chữ số
pureWeChatLogin: Đăng nhập bằng WeChat
pureWeiBoLogin: Đăng nhập bằng Weibo
menus:
pureAble: Able
pureAbnormal: Trang bất thường
pureAbout: Giới thiệu
pureAnimatecss: Lựa chọn AnimateCss
pureBarcode: Mã vạch
pureBoard: Bảng vẽ
pureButton: Hoạt hình nút
pureCardList: Trang danh sách thẻ
pureCascader: Cây địa phương
pureCheckButton: Nút kiểm tra
pureCheckCard: Thẻ kiểm tra
pureChildMenuOverflow: Hiển thị tooltip văn bản Menu con quá nhiều
pureCollapse: Thu gọn
pureColorHuntDoc: ColorHunt
pureColorPicker: Bảng chọn màu
pureComponents: Các thành phần
pureContextmenu: Menu ngữ cảnh
pureCountTo: Hiệu ứng số
pureCropping: Cắt ảnh
pureDanmaku: Danmaku
pureDatePicker: Chọn ngày
pureDateTimePicker: Chọn ngày giờ
pureDebounce: Debounce & Throttle
pureDept: Quản lý Bộ phận
pureDialog: Hộp thoại
pureDownload: Tải về
pureDraggable: Kéo thả
pureEditor: Biên tập viên
pureElButton: Nút
pureEmbeddedDoc: Tài liệu nhúng
pureEmpty: Trang rỗng
pureEpDoc: Element-Plus
pureExcel: Xuất Excel
pureExternalDoc: Tài liệu ngoài
pureExternalLink: Vue-Pure-Admin
pureExternalPage: Trang ngoài
pureFail: Trang thất bại
pureFive: '500'
pureFlowChart: Biểu đồ luồng
pureFormDesign: Thiết kế Form
pureFourZeroFour: '404'
pureFourZeroOne: '403'
pureGanttastic: Biểu đồ Gantt
pureGuide: Hướng dẫn
pureHome: Trang chủ
pureIconSelect: Lựa chọn biểu tượng
pureInfiniteScroll: Cuộn vô tận bảng
pureJsonEditor: Biên tập JSON
pureLineTree: Cây dòng
pureList: Trang danh sách
pureLogin: Đăng nhập
pureLoginLog: Log Đăng nhập
pureMap: Bản đồ
pureMenu1: Menu1
pureMenu1-1: Menu1-1
pureMenu1-2: Menu1-2
pureMenu1-2-1: Menu1-2-1
pureMenu1-2-2: Menu1-2-2
pureMenu1-3: Menu1-3
pureMenu2: Menu2
pureMenuOverflow: Hiển thị tooltip văn bản Menu quá nhiều
pureMenuTree: Cây Menu
pureMenus: Menu đa cấp
pureMessage: Lời nhắn
pureMindMap: Mind Map
pureMqtt: Mqtt Client
pureOnlineUser: Người dùng trực tuyến
pureOperationLog: Log Hoạt động
pureOptimize: Debounce, Throttle, Copy, Longpress Directives
purePdf: Xem trước PDF
purePermission: Quản lý Phân quyền
purePermissionButton: Phân quyền Nút
purePermissionPage: Phân quyền Trang
purePiniaDoc: Pinia
purePinyin: PinYin
purePrint: In
pureProgress: Tiến độ
pureQrcode: Mã QR
pureResult: Trang kết quả
pureRipple: Ripple
pureRole: Quản lý Vai trò
pureRouterDoc: Vue-Router
pureSchemaForm: Form
pureSeamless: Cuộn liền mạch
pureSegmented: Chia nhỏ
pureSelector: Lựa chọn phạm vi
pureSensitive: Bộ lọc nhạy cảm
pureSplitPane: Split Pane
pureStatistic: Thống kê
pureSuccess: Trang thành công
pureSwiper: Plugin Swiper
pureSysManagement: Quản lý Hệ thống
pureSysMonitor: Giám sát Hệ thống
pureSystemLog: Log Hệ thống
pureSystemMenu: Quản lý Menu
pureTable: Bảng
pureTableBase: Sử dụng cơ bản bảng
pureTableEdit: Sử dụng chỉnh sửa bảng
pureTableHigh: Sử dụng cao bảng
pureTabs: Quản lý Tab
pureTag: Thẻ
pureTailwindcssDoc: Tailwindcss
pureText: Chữ ellipsis
pureTimePicker: Chọn giờ
pureTimeline: Dòng thời gian
pureTypeit: Typeit
pureUiGradients: UiGradients
pureUpload: Tải lên tệp
pureUser: Quản lý Người dùng
pureUtilsLink: Pure-Admin-Utils
pureVerify: Mã xác nhận
pureVideo: Video
pureVideoFrame: Chụp Khung Video
pureVirtualList: Danh sách ảo
pureViteDoc: Vite
pureVueDoc: Vue3
pureVxeTable: Sử dụng ảo hóa bảng
pureWaterfall: Thác nước
pureWatermark: Water Mark
pureWavesurfer: Biểu đồ trực quan âm thanh
panel:
pureClearCache: Xóa Cache
pureClearCacheAndToLogin: Xóa bộ nhớ cache và quay lại trang đăng nhập
pureCloseSystemSet: Đóng Cấu hình Hệ thống
pureGreyModel: Giao diện xám
pureHiddenFooter: Ẩn footer
pureHiddenTags: Ẩn tab
pureHorizontalTip: Menu trên cùng, tổng quan súc tích
pureInterfaceDisplay: Hiển thị giao diện
pureLayoutModel: Giao diện bố cục
pureMixTip: Menu pha trộn, linh hoạt
pureMultiTagsCache: MultiTags Cache
pureOverallStyle: Kiểu tổng thể
pureOverallStyleDark: Tối
pureOverallStyleDarkTip: Làm việc dưới ánh trăng, thưởng thức sự thanh tao và tĩnh lặng của đêm
pureOverallStyleLight: Sáng
pureOverallStyleLightTip: Bắt đầu mới và sáng tạo giao diện làm việc thoải mái
pureOverallStyleSystem: Tự động
pureOverallStyleSystemTip: >-
Đồng bộ hóa thời gian, giao diện tự nhiên phản ứng với buổi sáng và hoàng
hôn
pureStretch: Kéo dài trang
pureStretchCustom: Tùy chỉnh
pureStretchCustomTip: Tối thiểu 1280, tối đa 1600
pureStretchFixed: Cố định
pureStretchFixedTip: Trang nhỏ gọn giúp dễ dàng tìm thông tin bạn cần
pureSystemSet: Cấu hình Hệ thống
pureTagsStyle: Kiểu tab
pureTagsStyleCard: Tab
pureTagsStyleCardTip: Tab cho việc duyệt web hiệu quả
pureTagsStyleChrome: Chrome
pureTagsStyleChromeTip: Kiểu Chrome mang phong cách cổ điển và thanh lịch
pureTagsStyleSmart: Thông minh
pureTagsStyleSmartTip: Thẻ thông minh thêm sự sáng tạo và lấp lánh
pureThemeColor: Màu chủ đề
pureVerticalTip: Menu bên trái quen thuộc và thân thiện
pureWeakModel: Mô hình yếu
search:
pureCollect: Thu gom
pureDragSort: Sắp xếp kéo thả
pureEmpty: Trống
pureHistory: Lịch sử
purePlaceholder: Tìm kiếm Menu
pureTotal: Tổng số
status:
pureLoad: Đang tải...
pureMessage: Thông báo
pureNoMessage: Không có thông báo
pureNoNotify: Không có thông báo
pureNoTodo: Không có công việc cần làm
pureNotify: Thông báo
pureTodo: Công việc cần làm

View File

@ -1,89 +0,0 @@
buttons:
pureLoginOut: 退出系统
pureLogin: 登录
pureOpenSystemSet: 打开系统配置
pureReload: 重新加载
pureCloseCurrentTab: 关闭当前标签页
pureCloseLeftTabs: 关闭左侧标签页
pureCloseRightTabs: 关闭右侧标签页
pureCloseOtherTabs: 关闭其他标签页
pureCloseAllTabs: 关闭全部标签页
pureContentFullScreen: 内容区全屏
pureContentExitFullScreen: 内容区退出全屏
pureClickCollapse: 点击折叠
pureClickExpand: 点击展开
pureConfirm: 确认
pureSwitch: 切换
pureClose: 关闭
pureBackTop: 回到顶部
pureOpenText:
pureCloseText:
search:
pureTotal:
pureHistory: 搜索历史
pureCollect: 收藏
pureDragSort: (可拖拽排序)
pureEmpty: 暂无搜索结果
purePlaceholder: 搜索菜单(支持拼音搜索)
panel:
pureSystemSet: 系统配置
pureCloseSystemSet: 关闭配置
pureClearCacheAndToLogin: 清空缓存并返回登录页
pureClearCache: 清空缓存
pureOverallStyle: 整体风格
pureOverallStyleLight: 浅色
pureOverallStyleLightTip: 清新启航,点亮舒适的工作界面
pureOverallStyleDark: 深色
pureOverallStyleDarkTip: 月光序曲,沉醉于夜的静谧雅致
pureOverallStyleSystem: 自动
pureOverallStyleSystemTip: 同步时光,界面随晨昏自然呼应
pureThemeColor: 主题色
pureLayoutModel: 导航模式
pureVerticalTip: 左侧菜单,亲切熟悉
pureHorizontalTip: 顶部菜单,简洁概览
pureMixTip: 混合菜单,灵活多变
pureStretch: 页宽
pureStretchFixed: 固定
pureStretchFixedTip: 紧凑页面,轻松找到所需信息
pureStretchCustom: 自定义
pureStretchCustomTip: 最小1280、最大1600
pureTagsStyle: 页签风格
pureTagsStyleSmart: 灵动
pureTagsStyleSmartTip: 灵动标签,添趣生辉
pureTagsStyleCard: 卡片
pureTagsStyleCardTip: 卡片标签,高效浏览
pureTagsStyleChrome: 谷歌
pureTagsStyleChromeTip: 谷歌风格,经典美观
pureInterfaceDisplay: 界面显示
pureGreyModel: 灰色模式
pureWeakModel: 色弱模式
pureHiddenTags: 隐藏标签页
pureHiddenFooter: 隐藏页脚
pureMultiTagsCache: 页签持久化
menus:
pureHome: 首页
pureLogin: 登录
pureAbnormal: 异常页面
pureFourZeroFour: "404"
pureFourZeroOne: "403"
pureFive: "500"
purePermission: 权限管理
purePermissionPage: 页面权限
purePermissionButton: 按钮权限
status:
pureLoad: 加载中...
pureMessage: 消息
pureNotify: 通知
pureTodo: 待办
pureNoMessage: 暂无消息
pureNoNotify: 暂无通知
pureNoTodo: 暂无待办
login:
pureUsername: 账号
purePassword: 密码
pureLogin: 登录
pureLoginSuccess: 登录成功
pureLoginFail: 登录失败
pureUsernameReg: 请输入账号
purePassWordReg: 请输入密码
purePassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合

View File

@ -45,6 +45,8 @@ export default defineFakeRoute([
response: () => {
return {
success: true,
msg: "success",
msgDev: null,
data: [permissionRouter]
};
}

View File

@ -50,8 +50,13 @@
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.4.7",
"@vue-flow/background": "^1.3.0",
"@vue-flow/core": "^1.38.2",
"@vueuse/core": "^10.11.0",
"@vueuse/motion": "^2.2.3",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
@ -70,8 +75,15 @@
"vue": "^3.4.31",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.0",
"xlsx": "^0.18.5",
"vue-tippy": "^6.4.4",
"vue-types": "^5.1.2"
"vue-types": "^5.1.2",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue-waterfall-plugin-next": "^2.4.3",
"vue3-danmaku": "^1.6.0",
"vue3-puzzle-vcode": "^1.1.7",
"vuedraggable": "^4.1.0",
"vxe-table": "4.6.17"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",

875
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true,
"Locale": "zh",
"Locale": "vi",
"Layout": "vertical",
"Theme": "light",
"DarkMode": false,

View File

@ -10,7 +10,7 @@ import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import { ReDialog } from "@/components/ReDialog";
import en from "element-plus/es/locale/lang/en";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import viVN from "element-plus/es/locale/lang/vi";
export default defineComponent({
name: "app",
@ -20,7 +20,7 @@ export default defineComponent({
},
computed: {
currentLocale() {
return this.$storage.locale?.locale === "zh" ? zhCn : en;
return this.$storage.locale?.locale === "vi" ? viVN : en;
}
}
});

View File

@ -2,7 +2,9 @@ import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
msg: string;
msgDev: string;
data: object;
};
export const getAsyncRoutes = () => {

View File

@ -3,19 +3,19 @@ import { http } from "@/utils/http";
export type UserResult = {
success: boolean;
data: {
/** 头像 */
/** Đường dẫn ảnh đại diện */
avatar: string;
/** 用户名 */
/** Tên người dùng */
username: string;
/** 昵称 */
/** Bút danh */
nickname: string;
/** 当前登录用户的角色 */
/** Các vai trò của người dùng hiện tại */
roles: Array<string>;
/** `token` */
/** Mã thông báo truy cập */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
/** Mã thông báo cần thiết để gọi API làm mới `accessToken` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
/** Thời gian hết hạn của `accessToken` (định dạng 'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
@ -23,21 +23,21 @@ export type UserResult = {
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
/** Mã thông báo truy cập */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
/** Mã thông báo cần thiết để gọi API làm mới `accessToken` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
/** Thời gian hết hạn của `accessToken` (định dạng 'xxxx/xx/xx xx:xx:xx') */
expires: Date;
};
};
/** 登录 */
/** Đăng nhập */
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/login", { data });
};
/** 刷新`token` */
/** Làm mới `token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};

View File

@ -1,7 +1,7 @@
import { ElCol } from "element-plus";
import { h, defineComponent } from "vue";
// 封装element-plus的el-col组件
// Bao bọc thành phần ElCol của element-plus
export default defineComponent({
name: "ReCol",
props: {

View File

@ -0,0 +1,2 @@
normal 普通数字动画组件
rebound 回弹式数字动画组件

View File

@ -0,0 +1,11 @@
import reNormalCountTo from "./src/normal";
import reboundCountTo from "./src/rebound";
import { withInstall } from "@pureadmin/utils";
/** 普通数字动画组件 */
const ReNormalCountTo = withInstall(reNormalCountTo);
/** 回弹式数字动画组件 */
const ReboundCountTo = withInstall(reboundCountTo);
export { ReNormalCountTo, ReboundCountTo };

View File

@ -0,0 +1,179 @@
import {
watch,
unref,
computed,
reactive,
onMounted,
defineComponent
} from "vue";
import { countToProps } from "./props";
import { isNumber } from "@pureadmin/utils";
export default defineComponent({
name: "ReNormalCountTo",
props: countToProps,
emits: ["mounted", "callback"],
setup(props, { emit }) {
const state = reactive<{
localStartVal: number;
printVal: number | null;
displayValue: string;
paused: boolean;
localDuration: number | null;
startTime: number | null;
timestamp: number | null;
rAF: any;
remaining: number | null;
color: string;
fontSize: string;
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null,
color: null,
fontSize: "16px"
});
const getCountDown = computed(() => {
return props.startVal > props.endVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
function start() {
const { startVal, duration, color, fontSize } = props;
state.localStartVal = startVal;
state.startTime = null;
state.localDuration = duration;
state.paused = false;
state.color = color;
state.fontSize = fontSize;
state.rAF = requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function pauseResume() {
if (state.paused) {
resume();
state.paused = false;
} else {
pause();
state.paused = true;
}
}
function pause() {
cancelAnimationFrame(state.rAF);
}
function resume() {
state.startTime = null;
state.localDuration = +(state.remaining as number);
state.localStartVal = +(state.printVal as number);
requestAnimationFrame(count);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function reset() {
state.startTime = null;
cancelAnimationFrame(state.rAF);
state.displayValue = formatNumber(props.startVal);
}
function count(timestamp: number) {
const { useEasing, easingFn, endVal } = props;
if (!state.startTime) state.startTime = timestamp;
state.timestamp = timestamp;
const progress = timestamp - state.startTime;
state.remaining = (state.localDuration as number) - progress;
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(
progress,
0,
state.localStartVal - endVal,
state.localDuration as number
);
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
);
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) *
(progress / (state.localDuration as number));
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) *
(progress / (state.localDuration as number));
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal;
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal;
}
state.displayValue = formatNumber(state.printVal);
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count);
} else {
emit("callback");
}
}
function formatNumber(num: number | string) {
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += "";
const x = num.split(".");
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : "";
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, "$1" + separator + "$2");
}
}
return prefix + x1 + x2 + suffix;
}
onMounted(() => {
if (props.autoplay) {
start();
}
emit("mounted");
});
return () => (
<>
<span
style={{
color: props.color,
fontSize: props.fontSize
}}
>
{state.displayValue}
</span>
</>
);
}
});

View File

@ -0,0 +1,32 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const countToProps = {
startVal: propTypes.number.def(0),
endVal: propTypes.number.def(2020),
duration: propTypes.number.def(1300),
autoplay: propTypes.bool.def(true),
decimals: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value >= 0;
}
},
color: propTypes.string.def(),
fontSize: propTypes.string.def(),
decimal: propTypes.string.def("."),
separator: propTypes.string.def(","),
prefix: propTypes.string.def(""),
suffix: propTypes.string.def(""),
useEasing: propTypes.bool.def(true),
easingFn: {
type: Function as PropType<
(t: number, b: number, c: number, d: number) => number
>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
}
}
};

View File

@ -0,0 +1,72 @@
import "./rebound.css";
import {
ref,
unref,
onBeforeMount,
defineComponent,
onBeforeUnmount
} from "vue";
import { reboundProps } from "./props";
export default defineComponent({
name: "ReboundCountTo",
props: reboundProps,
setup(props) {
const ulRef = ref();
const timer = ref(null);
onBeforeMount(() => {
const ua = navigator.userAgent.toLowerCase();
const testUA = regexp => regexp.test(ua);
const isSafari = testUA(/safari/g) && !testUA(/chrome/g);
// Safari浏览器的兼容代码
isSafari &&
(timer.value = setTimeout(() => {
ulRef.value.setAttribute(
"style",
`
animation: none;
transform: translateY(calc(var(--i) * -9.09%))
`
);
}, props.delay * 1000));
});
onBeforeUnmount(() => {
clearTimeout(unref(timer));
});
return () => (
<>
<div
class="scroll-num"
style={{ "--i": props.i, "--delay": props.delay }}
>
<ul ref="ulRef" style={{ fontSize: "32px" }}>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
<svg width="0" height="0">
<filter id="blur">
<feGaussianBlur
in="SourceGraphic"
stdDeviation={`0 ${props.blur}`}
/>
</filter>
</svg>
</div>
</>
);
}
});

View File

@ -0,0 +1,15 @@
import type { PropType } from "vue";
import propTypes from "@/utils/propTypes";
export const reboundProps = {
delay: propTypes.number.def(1),
blur: propTypes.number.def(2),
i: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value < 10 && value >= 0 && Number.isInteger(value);
}
}
};

View File

@ -0,0 +1,77 @@
.scroll-num {
width: var(--width, 20px);
height: var(--height, calc(var(--width, 20px) * 1.8));
color: var(--color, #333);
font-size: var(--height, calc(var(--width, 20px) * 1.1));
line-height: var(--height, calc(var(--width, 20px) * 1.8));
text-align: center;
overflow: hidden;
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
ul {
animation:
move 0.3s linear infinite,
bounce-in-down 1s calc(var(--delay) * 1s) forwards;
}
@keyframes move {
from {
transform: translateY(-90%);
filter: url(#blur);
}
to {
transform: translateY(1%);
filter: url(#blur);
}
}
@keyframes bounce-in-down {
from {
transform: translateY(calc(var(--i) * -9.09% - 7%));
filter: none;
}
25% {
transform: translateY(calc(var(--i) * -9.09% + 3%));
}
50% {
transform: translateY(calc(var(--i) * -9.09% - 1%));
}
70% {
transform: translateY(calc(var(--i) * -9.09% + 0.6%));
}
85% {
transform: translateY(calc(var(--i) * -9.09% - 0.3%));
}
to {
transform: translateY(calc(var(--i) * -9.09%));
}
}
@keyframes enhance-bounce-in-down {
25% {
transform: translateY(8%);
}
50% {
transform: translateY(-4%);
}
70% {
transform: translateY(2%);
}
85% {
transform: translateY(-1%);
}
to {
transform: translateY(0);
}
}

View File

@ -12,7 +12,7 @@ import type {
const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */
/** Mở hộp thoại */
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
@ -25,7 +25,7 @@ const addDialog = (options: DialogOptions) => {
}
};
/** 关闭弹框 */
/** Đóng hộp thoại */
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value[index].visible = false;
options.closeCallBack && options.closeCallBack({ options, index, args });
@ -37,21 +37,21 @@ const closeDialog = (options: DialogOptions, index: number, args?: any) => {
};
/**
* @description
* @param value
* @param key `title`
* @param index `0``index`
* @description Thay đi giá trị thuộc tính của hộp thoại
* @param value Giá trị thuộc tính
* @param key Thuộc tính (mặc đnh `title`)
* @param index Chỉ số của hộp thoại (mặc đnh `0`, đi diện cho hộp thoại duy nhất; đ thay đi thuộc tính của hộp thoại lồng nhau, gán chỉ số của hộp thoại đó cho `index`)
*/
const updateDialog = (value: any, key = "title", index = 0) => {
dialogStore.value[index][key] = value;
};
/** 关闭所有弹框 */
/** Đóng tất cả hộp thoại */
const closeAllDialog = () => {
dialogStore.value = [];
};
/** 使`addDialog`
/** Chc chn phi nhp và đăng ký ba dưi đây, yên tâm đăng ký, không s dng `addDialog` thì s không b gn kết
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22

View File

@ -23,7 +23,7 @@ const footerButtons = computed(() => {
? options.footerButtons
: ([
{
label: "取消",
label: "Hủy",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
@ -37,7 +37,7 @@ const footerButtons = computed(() => {
}
},
{
label: "确定",
label: "OK",
type: "primary",
text: true,
bg: true,

View File

@ -0,0 +1,39 @@
.point {
width: var(--point-width);
height: var(--point-height);
background: var(--point-background);
position: relative;
border-radius: var(--point-border-radius);
}
.point-flicker:after {
background: var(--point-background);
}
.point-flicker:before,
.point-flicker:after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: var(--point-border-radius);
animation: flicker 1.2s ease-out infinite;
}
@keyframes flicker {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 1;
}
100% {
transform: scale(var(--point-scale));
opacity: 0;
}
}

View File

@ -0,0 +1,44 @@
import "./index.css";
import { type Component, h, defineComponent } from "vue";
export interface attrsType {
width?: string;
height?: string;
borderRadius?: number | string;
background?: string;
scale?: number | string;
}
/**
*
* @param width string
* @param height string
* @param borderRadius number | string 050%
* @param background string
* @param scale number | string 2
* @returns Component
*/
export function useRenderFlicker(attrs?: attrsType): Component {
return defineComponent({
name: "ReFlicker",
render() {
return h(
"div",
{
class: "point point-flicker",
style: {
"--point-width": attrs?.width ?? "12px",
"--point-height": attrs?.height ?? "12px",
"--point-background":
attrs?.background ?? "var(--el-color-primary)",
"--point-border-radius": attrs?.borderRadius ?? "50%",
"--point-scale": attrs?.scale ?? "2"
}
},
{
default: () => []
}
);
}
});
}

View File

@ -2,11 +2,11 @@ import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont";
/** 本地图标组件 */
/** icon offline*/
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
/** icon online */
const IconifyIconOnline = iconifyIconOnline;
/** `iconfont`组件 */
/** `iconfont` */
const FontIcon = fontIcon;
export { IconifyIconOffline, IconifyIconOnline, FontIcon };

View File

@ -3,16 +3,16 @@ import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* `iconfont` `svg` `iconify`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
* @param icon
* @param attrs iconType
* Hỗ trợ `iconfont`, `svg` tùy chỉnh tất cả các biểu tượng trong `iconify`
* @see Nhấp đ xem hướng dẫn biểu tượng trong tài liệu {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
* @param icon Bắt buộc Biểu tượng
* @param attrs Tùy chọn Các thuộc tính loại iconType
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
// typeof icon === "function" belongs to SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
@ -45,7 +45,7 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
}
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
// Kiểm tra dấu : để xác định biểu tượng trực tuyến hoặc cục bộ, có tồn tại thì là biểu tượng trực tuyến, ngược lại là cục bộ
return defineComponent({
name: "Icon",
render() {

View File

@ -1,6 +1,6 @@
import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
// Đóng gói thành phần iconfont, mặc định sử dụng chế độ tham chiếu `font-class`, hỗ trợ tham chiếu `unicode`, `font-class`, `symbol` (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code)
export default defineComponent({
name: "FontIcon",
props: {

View File

@ -1,7 +1,7 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// Iconify Icon在Vue里本地使用用于内网环境
// Biểu tượng Iconify được sử dụng cục bộ trong Vue (dành cho môi trường mạng nội bộ)
export default defineComponent({
name: "IconifyIconOffline",
components: { IconifyIcon },

View File

@ -1,7 +1,7 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境
// Biểu tượng Iconify được sử dụng cục bộ trong Vue (dành cho môi trường mạng)
export default defineComponent({
name: "IconifyIconOnline",
components: { IconifyIcon },

View File

@ -1,12 +1,14 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
// Đây là nơi lưu trữ biểu tượng cục bộ, được tải trong tệp src/layout/index.vue để tránh tải lại khi khởi động lần đầu
import { addIcon } from "@iconify/vue/dist/offline";
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
// Biểu tượng menu cục bộ, máy chủ trả về chuỗi biểu tượng tương ứng trong thuộc tính icon của định tuyến và phía trước sử dụng addIcon để thêm vào đây để hiển thị biểu tượng menu
// @iconify-icons/ep
import Lollipop from "@iconify-icons/ep/lollipop";
import HomeFilled from "@iconify-icons/ep/home-filled";
addIcon("ep:lollipop", Lollipop);
addIcon("ep:home-filled", HomeFilled);
// @iconify-icons/ri
import Search from "@iconify-icons/ri/search-line";
import InformationLine from "@iconify-icons/ri/information-line";

View File

@ -13,7 +13,7 @@ export interface iconType {
align?: string;
onLoad?: Function;
includes?: Function;
// svg 需要什么SVG属性自行添加
// svg Những thuộc tính SVG nào là cần thiết để bạn thêm vào?
fill?: string;
// all icon
style?: object;

View File

@ -1,5 +1,5 @@
import pureTableBar from "./src/bar";
import { withInstall } from "@pureadmin/utils";
/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
/** dùng `@pureadmin/table` để thực hiện các thao tác trên bảng nhanh chóng và thuận tiện https://github.com/pure-admin/pure-admin-table */
export const PureTableBar = withInstall(pureTableBar);

View File

@ -25,24 +25,26 @@ import SettingIcon from "@/assets/table-bar/settings.svg?component";
import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
const props = {
/** 头部最左边的标题 */
/** Tiêu đề ở phía trên bên trái của bảng */
title: {
type: String,
default: "列表"
default: "Danh sách"
},
/** 对于树形表格如果想启用展开和折叠功能传入当前表格的ref即可 */
/** Nếu muốn bật chức năng mở rộng và thu gọn cho bảng cây, hãy truyền vào tham chiếu hiện tại của bảng */
tableRef: {
type: Object as PropType<any>
},
/** 需要展示的列 */
/** Các cột cần hiển thị */
columns: {
type: Array as PropType<TableColumnList>,
default: () => []
},
/** Cho biết có mở rộng tất cả các hàng ban đầu hay không */
isExpandAll: {
type: Boolean,
default: true
},
/** Khóa duy nhất cho bảng */
tableKey: {
type: [String, Number] as PropType<string | number>,
default: "0"
@ -161,25 +163,25 @@ export default defineComponent({
style={getDropdownItemStyle.value("large")}
onClick={() => (size.value = "large")}
>
Lỏng lẻo
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("default")}
onClick={() => (size.value = "default")}
>
Mặc đnh
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("small")}
onClick={() => (size.value = "small")}
>
Gọn nhẹ
</el-dropdown-item>
</el-dropdown-menu>
)
};
/** 列展示拖拽排序 */
/** Cột hiển thị kéo và thả sắp xếp */
const rowDrop = (event: { preventDefault: () => void }) => {
event.preventDefault();
nextTick(() => {
@ -195,7 +197,7 @@ export default defineComponent({
const oldColumn = dynamicColumns.value[oldIndex];
const newColumn = dynamicColumns.value[newIndex];
if (oldColumn?.fixed || newColumn?.fixed) {
// 当前列存在fixed属性 则不可拖拽
// Nếu cột hiện tại có thuộc tính cố định thì không thể kéo được.
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
if (newIndex > oldIndex) {
wrapperElem.insertBefore(targetThElem, oldThElem);
@ -237,7 +239,7 @@ export default defineComponent({
reference: () => (
<SettingIcon
class={["w-[16px]", iconClass.value]}
v-tippy={rendTippyProps("列设置")}
v-tippy={rendTippyProps("Cài đặt cột")}
/>
)
};
@ -263,7 +265,7 @@ export default defineComponent({
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
}}
v-tippy={rendTippyProps(
isExpandAll.value ? "折叠" : "展开"
isExpandAll.value ? "Thu nhỏ" : "Mở rộng"
)}
onClick={() => onExpand()}
/>
@ -276,14 +278,14 @@ export default defineComponent({
iconClass.value,
loading.value ? "animate-spin" : ""
]}
v-tippy={rendTippyProps("刷新")}
v-tippy={rendTippyProps("Đặt lại")}
onClick={() => onReFresh()}
/>
<el-divider direction="vertical" />
<el-dropdown
v-slots={dropdown}
trigger="click"
v-tippy={rendTippyProps("密度")}
v-tippy={rendTippyProps("Tỉ trọng")}
>
<CollapseIcon class={["w-[16px]", iconClass.value]} />
</el-dropdown>
@ -299,13 +301,13 @@ export default defineComponent({
<div class={[topClass.value]}>
<el-checkbox
class="!-mr-1"
label="列展示"
label="Hiển thị cột"
v-model={checkAll.value}
indeterminate={isIndeterminate.value}
onChange={value => handleCheckAllChange(value)}
/>
<el-button type="primary" link onClick={() => onReset()}>
Đt lại
</el-button>
</div>

View File

@ -1,7 +1,7 @@
import reSegmented from "./src/index";
import { withInstall } from "@pureadmin/utils";
/** 分段控制器组件 */
/** Thành phần điều khiển phân đoạn */
export const ReSegmented = withInstall(reSegmented);
export default ReSegmented;

View File

@ -23,27 +23,27 @@ const props = {
type: Array<OptionsType>,
default: () => []
},
/** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */
/** Mặc định được chọn, theo mô hình chỉ số đầu tiên là `0`, có thể chọn (Khi `modelValue` chỉ có kiểu `number` thì mới có sự phản hồi) */
modelValue: {
type: undefined,
require: false,
default: "0"
},
/** 将宽度调整为父元素宽度 */
/** Thay đổi chiều rộng để phù hợp với chiều rộng của phần tử cha */
block: {
type: Boolean,
default: false
},
/** 控件尺寸 */
/** Kích thước của điều khiển */
size: {
type: String as PropType<"small" | "default" | "large">
},
/** 是否全局禁用,默认 `false` */
/** Vô hiệu hóa toàn cầu, mặc định `false` */
disabled: {
type: Boolean,
default: false
},
/** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */
/** Khi nội dung thay đổi, thiết lập `resize` để tự điều chỉnh vị trí của nó trong bộ chứa */
resize: {
type: Boolean,
default: false

View File

@ -2,19 +2,19 @@ import type { VNode, Component } from "vue";
import type { iconType } from "@/components/ReIcon/src/types.ts";
export interface OptionsType {
/** 文字 */
/** Nhãn */
label?: string | (() => VNode | Component);
/**
* @description `useRenderIcon`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
* @description Biểu tượng, đưc render bằng hàm `useRenderIcon` đưc tích hợp trong nền tảng
* @see {@link Xem thêm tại https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
/** Thuộc tính và style của biểu tượng */
iconAttrs?: iconType;
/** */
/** Giá trị */
value?: any;
/** 是否禁用 */
/** Đã bị vô hiệu hóa hay chưa */
disabled?: boolean;
/** `tooltip` 提示 */
/** Chú thích tooltip */
tip?: string;
}

View File

@ -1,7 +1,7 @@
import reText from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
/** 支持`Tooltip`提示的文本省略组件 */
/** Thành phần cắt ngắn văn bản hỗ trợ Tooltip */
export const ReText = withInstall(reText);
export default ReText;

View File

@ -7,7 +7,7 @@ defineOptions({
});
const props = defineProps({
//
// S dòng
lineClamp: {
type: [String, Number]
},
@ -24,10 +24,10 @@ const tippyFunc = ref();
const isTextEllipsis = (el: HTMLElement) => {
if (!props.lineClamp) {
//
// Kim tra có dòng đơn b ct ngn
return el.scrollWidth > el.clientWidth;
} else {
//
// Kim tra có dòng nhiu b ct ngn
return el.scrollHeight > el.clientHeight;
}
};

View File

@ -26,7 +26,7 @@ const getConfig = (key?: string): PlatformConfigs => {
return config;
};
/** 获取项目动态全局配置 */
/** Lấy cấu hình toàn cầu động của dự án */
export const getPlatformConfig = async (app: App): Promise<undefined> => {
app.config.globalProperties.$config = getConfig();
return axios({
@ -35,21 +35,21 @@ export const getPlatformConfig = async (app: App): Promise<undefined> => {
})
.then(({ data: config }) => {
let $config = app.config.globalProperties.$config;
// 自动注入系统配置
// Tự động chèn cấu hình hệ thống
if (app && $config && typeof config === "object") {
$config = Object.assign($config, config);
app.config.globalProperties.$config = $config;
// 设置全局配置
// Đặt cấu hình toàn cầu
setConfig($config);
}
return $config;
})
.catch(() => {
throw "请在public文件夹下添加platform-config.json配置文件";
throw "Vui lòng thêm tệp cấu hình platform-config.json trong thư mục public";
});
};
/** 本地响应式存储的命名空间 */
/** Không gian lưu trữ đáp ứng cục bộ */
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
export { getConfig, setConfig, responsiveStorageNameSpace };

View File

@ -7,23 +7,23 @@ export interface CopyEl extends HTMLElement {
copyValue: string;
}
/** 文本复制指令(默认双击复制) */
/** Chỉ thị sao chép văn bản (mặc định là sao chép khi double click) */
export const copy: Directive = {
mounted(el: CopyEl, binding: DirectiveBinding<string>) {
const { value } = binding;
if (value) {
el.copyValue = value;
const arg = binding.arg ?? "dblclick";
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
// Đăng ký sử dụng addEventListener khi mounted và tự động gỡ bỏ addEventListener khi unmounted
useEventListener(el, arg, () => {
const success = copyTextToClipboard(el.copyValue);
success
? message("复制成功", { type: "success" })
: message("复制失败", { type: "error" });
? message("Sao chép thành công", { type: "success" })
: message("Sao chép thất bại", { type: "error" });
});
} else {
throw new Error(
'[Directive: copy]: need value! Like v-copy="modelValue"'
'[Directive: copy]: cần giá trị! Ví dụ: v-copy="modelValue"'
);
}
},

View File

@ -9,19 +9,19 @@ import { useEventListener } from "@vueuse/core";
import type { Directive, DirectiveBinding } from "vue";
export interface OptimizeOptions {
/** 事件名 */
/** Tên sự kiện */
event: string;
/** 事件触发的方法 */
/** Phương thức gọi sự kiện */
fn: (...params: any) => any;
/** 是否立即执行 */
/** Có thực thi ngay lập tức hay không */
immediate?: boolean;
/** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */
/** Thời gian chậm hành động hoặc giảm tốc (giảm tốc mặc định: `200` mili giây, giảm tốc mặc định: `1000` mili giây) */
timeout?: number;
/** 传递的参数 */
/** Tham số truyền vào */
params?: any;
}
/** 防抖v-optimize或v-optimize:debounce、节流v-optimize:throttle指令 */
/** Chỉ thị giảm tốc (v-optimize hoặc v-optimize:giảm tốc), giảm tốc (v-optimize:giảm tốc) */
export const optimize: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding<OptimizeOptions>) {
const { value } = binding;
@ -35,11 +35,11 @@ export const optimize: Directive = {
params = isObject(params) ? Array.of(params) : params;
} else {
throw new Error(
"[Directive: optimize]: `params` must be an array or object"
"[Chỉ thị: optimize]: `params` phải là một mảng hoặc đối tượng"
);
}
}
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
// Đăng ký sử dụng addEventListener khi mounted và tự động gỡ bỏ addEventListener khi unmounted
useEventListener(
el,
value.event,
@ -56,12 +56,12 @@ export const optimize: Directive = {
);
} else {
throw new Error(
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
"[Chỉ thị: optimize]: `event` và `fn` là bắt buộc, và `fn` phải là một hàm"
);
}
} else {
throw new Error(
"[Directive: optimize]: only `debounce` and `throttle` are supported"
"[Chỉ thị: optimize]: chỉ hỗ trợ `debounce` và `throttle`"
);
}
}

View File

@ -189,7 +189,7 @@ const transitionMain = defineComponent({
</template>
</router-view>
<!-- 页脚 -->
<!-- Chân trang -->
<LayFooter v-if="!hideFooter && !fixedHeader" />
</section>
</template>

View File

@ -48,9 +48,9 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
<LayNavMix v-if="layout === 'mix'" />
<div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<!-- Menu Search -->
<LaySearch id="header-search" />
<!-- 国际化 -->
<!-- Globalization -->
<el-dropdown id="header-translation" trigger="click">
<GlobalizationIcon
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
@ -58,16 +58,16 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:style="getDropdownItemStyle(locale, 'vi')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
@click="translationCh"
>
<IconifyIconOffline
v-show="locale === 'zh'"
class="check-zh"
v-show="locale === 'vi'"
class="check-vi"
:icon="Check"
/>
简体中文
Tiếng Việt
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@ -82,11 +82,11 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 全屏 -->
<!-- Full Screen -->
<LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 -->
<!-- Notification -->
<LayNotice id="header-notice" />
<!-- 退出登录 -->
<!-- Logout -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<img :src="userAvatar" :style="avatarsStyle" />
@ -168,7 +168,7 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
padding: 5px 40px;
}
.check-zh {
.check-vi {
position: absolute;
left: 20px;
}

View File

@ -65,9 +65,9 @@ nextTick(() => {
/>
</el-menu>
<div class="horizontal-header-right">
<!-- 菜单搜索 -->
<!-- Menu Search -->
<LaySearch id="header-search" />
<!-- 国际化 -->
<!-- Globalization -->
<el-dropdown id="header-translation" trigger="click">
<GlobalizationIcon
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
@ -75,14 +75,14 @@ nextTick(() => {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:style="getDropdownItemStyle(locale, 'vi')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
@click="translationCh"
>
<span v-show="locale === 'zh'" class="check-zh">
<span v-show="locale === 'vi'" class="check-vi">
<IconifyIconOffline :icon="Check" />
</span>
简体中文
Tiếng Việt
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@ -97,11 +97,11 @@ nextTick(() => {
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 全屏 -->
<!-- Full Screen -->
<LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 -->
<!-- Notification -->
<LayNotice id="header-notice" />
<!-- 退出登录 -->
<!-- Logout -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover">
<img :src="userAvatar" :style="avatarsStyle" />
@ -140,7 +140,7 @@ nextTick(() => {
padding: 5px 40px;
}
.check-zh {
.check-vi {
position: absolute;
left: 20px;
}

View File

@ -38,7 +38,7 @@ const {
function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus;
/** 当前路由的父级路径 */
/** Current route's parent paths */
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = !isAllEmpty(route.meta?.activePath)
? route.meta.activePath
@ -99,9 +99,9 @@ watch(
</el-menu-item>
</el-menu>
<div class="horizontal-header-right">
<!-- 菜单搜索 -->
<!-- Menu Search -->
<LaySearch id="header-search" />
<!-- 国际化 -->
<!-- Globalization -->
<el-dropdown id="header-translation" trigger="click">
<GlobalizationIcon
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
@ -109,14 +109,14 @@ watch(
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:style="getDropdownItemStyle(locale, 'vi')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
@click="translationCh"
>
<span v-show="locale === 'zh'" class="check-zh">
<span v-show="locale === 'vi'" class="check-vi">
<IconifyIconOffline :icon="Check" />
</span>
简体中文
Tiếng Việt
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@ -131,11 +131,11 @@ watch(
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 全屏 -->
<!-- Full Screen -->
<LaySidebarFullScreen id="full-screen" />
<!-- 消息通知 -->
<!-- Notification -->
<LayNotice id="header-notice" />
<!-- 退出登录 -->
<!-- Logout -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<img :src="userAvatar" :style="avatarsStyle" />
@ -174,7 +174,7 @@ watch(
padding: 5px 40px;
}
.check-zh {
.check-vi {
position: absolute;
left: 20px;
}

View File

@ -18,21 +18,21 @@ import {
export function useDataThemeChange() {
const { layoutTheme, layout } = useLayout();
const themeColors = ref<Array<themeColorsType>>([
/* 亮白色 */
/* Màu sáng trắng */
{ color: "#ffffff", themeColor: "light" },
/* 道奇蓝 */
/* Xanh Đậm */
{ color: "#1b2a47", themeColor: "default" },
/* 深紫罗兰色 */
/* Tím Đậm */
{ color: "#722ed1", themeColor: "saucePurple" },
/* 深粉色 */
/* Hồng Đậm */
{ color: "#eb2f96", themeColor: "pink" },
/* 猩红色 */
/* Đỏ Lửa */
{ color: "#f5222d", themeColor: "dusk" },
/* 橙红色 */
/* Cam Đỏ */
{ color: "#fa541c", themeColor: "volcano" },
/* 绿宝石 */
/* Ngọc Lục Bảo */
{ color: "#13c2c2", themeColor: "mingQing" },
/* 酸橙绿 */
/* Xanh Lục Giác */
{ color: "#52c41a", themeColor: "auroraGreen" }
]);
@ -48,7 +48,7 @@ export function useDataThemeChange() {
targetEl.className = flag ? `${className} ${clsName}` : className;
}
/** 设置导航主题色 */
/** Set màu chủ đề điều hướng */
function setLayoutThemeColor(
theme = getConfig().Theme ?? "light",
isClick = true
@ -57,7 +57,7 @@ export function useDataThemeChange() {
toggleTheme({
scopeName: `layout-theme-${theme}`
});
// 如果非isClick保留之前的themeColor
// Nếu không phải là isClick, giữ lại themeColor trước đó
const storageThemeColor = $storage.layout.themeColor;
$storage.layout = {
layout: layout.value,
@ -84,7 +84,7 @@ export function useDataThemeChange() {
);
}
/** 设置 `element-plus` 主题色 */
/** Set màu chủ đề `element-plus` */
const setEpThemeColor = (color: string) => {
useEpThemeStoreHook().setEpThemeColor(color);
document.documentElement.style.setProperty("--el-color-primary", color);
@ -96,7 +96,7 @@ export function useDataThemeChange() {
}
};
/** 浅色、深色整体风格切换 */
/** Chuyển đổi kiểu chủ đề sáng, tối */
function dataThemeChange(overall?: string) {
overallStyle.value = overall;
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
@ -115,7 +115,7 @@ export function useDataThemeChange() {
}
}
/** 清空缓存并返回登录页 */
/** Xóa bộ nhớ cache và quay lại trang đăng nhập */
function onReset() {
removeToken();
storageLocal().clear();

View File

@ -1,37 +1,35 @@
import App from "./App.vue";
import router from "./router";
import { setupStore } from "@/store";
import { useI18n } from "@/plugins/i18n";
import { getPlatformConfig } from "./config";
import { MotionPlugin } from "@vueuse/motion";
// import { useEcharts } from "@/plugins/echarts";
import { createApp, type Directive } from "vue";
import { useElementPlus } from "@/plugins/elementPlus";
import { injectResponsiveStorage } from "@/utils/responsive";
import App from "./App.vue"; // Nhập component App.vue chính của ứng dụng
import router from "./router"; // Nhập định tuyến của Vue Router
import { setupStore } from "@/store"; // Sử dụng hàm setupStore từ module store
import { useI18n } from "@/plugins/i18n"; // Sử dụng plugin i18n cho quản lý ngôn ngữ
import { getPlatformConfig } from "./config"; // Lấy cấu hình nền tảng từ module config
import { MotionPlugin } from "@vueuse/motion"; // Sử dụng plugin motion từ VueUse
import { useEcharts } from "@/plugins/echarts"; // Sử dụng plugin Echarts
import { createApp, type Directive } from "vue"; // Tạo ứng dụng Vue mới
import { useVxeTable } from "@/plugins/vxeTable"; // Sử dụng plugin VxeTable
import { useElementPlus } from "@/plugins/elementPlus"; // Sử dụng plugin Element Plus
import { injectResponsiveStorage } from "@/utils/responsive"; // Sử dụng hàm injectResponsiveStorage từ util responsive
import Table from "@pureadmin/table";
// import PureDescriptions from "@pureadmin/descriptions";
import Table from "@pureadmin/table"; // Nhập component Table từ thư viện @pureadmin/table
import PureDescriptions from "@pureadmin/descriptions"; // Nhập component PureDescriptions từ thư viện @pureadmin/descriptions
// 引入重置样式
// Nhập các file style để reset, import index.scss và tailwind.css
import "./style/reset.scss";
// 导入公共样式
import "./style/index.scss";
// 一定要在main.ts中导入tailwind.css防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题
import "./style/tailwind.css";
import "element-plus/dist/index.css";
// 导入字体图标
import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css";
import "element-plus/dist/index.css"; // Import CSS của Element Plus
import "./assets/iconfont/iconfont.js"; // Nhập icon font
import "./assets/iconfont/iconfont.css"; // Import CSS của icon font
const app = createApp(App);
const app = createApp(App); // Tạo một ứng dụng Vue mới với component App.vue
// 自定义指令
// Định nghĩa các custom directive
import * as directives from "@/directives";
Object.keys(directives).forEach(key => {
app.directive(key, (directives as { [key: string]: Directive })[key]);
});
// 全局注册@iconify/vue图标库
// Đăng ký toàn cục thư viện iconify/vue
import {
IconifyIconOffline,
IconifyIconOnline,
@ -41,23 +39,29 @@ app.component("IconifyIconOffline", IconifyIconOffline);
app.component("IconifyIconOnline", IconifyIconOnline);
app.component("FontIcon", FontIcon);
// 全局注册按钮级别权限组件
// Đăng ký toàn cục component cho quyền truy cập cấp nút
import { Auth } from "@/components/ReAuth";
app.component("Auth", Auth);
// 全局注册vue-tippy
// Đăng ký toàn cục Vue Tippy
import "tippy.js/dist/tippy.css";
import "tippy.js/themes/light.css";
import VueTippy from "vue-tippy";
app.use(VueTippy);
// Lấy cấu hình nền tảng và khởi tạo ứng dụng
getPlatformConfig(app).then(async config => {
setupStore(app);
app.use(router);
await router.isReady();
injectResponsiveStorage(app, config);
app.use(MotionPlugin).use(useI18n).use(useElementPlus).use(Table);
// .use(PureDescriptions)
// .use(useEcharts);
app.mount("#app");
setupStore(app); // Thiết lập store Vuex
app.use(router); // Sử dụng router Vue
await router.isReady(); // Chờ router sẵn sàng
injectResponsiveStorage(app, config); // Inject cấu hình đáp ứng vào ứng dụng
app
.use(MotionPlugin) // Sử dụng MotionPlugin từ VueUse
.use(useI18n) // Sử dụng plugin i18n
.use(useElementPlus) // Sử dụng plugin Element Plus
.use(Table) // Sử dụng component Table
.use(useVxeTable) // Sử dụng plugin VxeTable
.use(PureDescriptions) // Sử dụng component PureDescriptions
.use(useEcharts); // Sử dụng plugin Echarts
app.mount("#app"); // Gắn ứng dụng vào phần tử có id là "app"
});

View File

@ -1,15 +1,15 @@
// 多组件库的国际化和本地项目国际化兼容
// Internationalization compatibility for multiple component libraries and local projects
import { type I18n, createI18n } from "vue-i18n";
import type { App, WritableComputedRef } from "vue";
import { responsiveStorageNameSpace } from "@/config";
import { storageLocal, isObject } from "@pureadmin/utils";
// element-plus国际化
// Element Plus internationalization
import enLocale from "element-plus/es/locale/lang/en";
import zhLocale from "element-plus/es/locale/lang/zh-cn";
import zhLocale from "element-plus/es/locale/lang/vi";
const siphonI18n = (function () {
// 仅初始化一次国际化配置
// Initialize internationalization configuration only once
let cache = Object.fromEntries(
Object.entries(
import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
@ -18,14 +18,14 @@ const siphonI18n = (function () {
return [matched, value.default];
})
);
return (prefix = "zh-CN") => {
return (prefix = "vi") => {
return cache[prefix];
};
})();
export const localesConfigs = {
zh: {
...siphonI18n("zh-CN"),
vi: {
...siphonI18n("vi"),
...zhLocale
},
en: {
@ -34,7 +34,7 @@ export const localesConfigs = {
}
};
/** 获取对象中所有嵌套对象的key键并将它们用点号分割组成字符串 */
/** Get keys of all nested objects in an object and concatenate them with dots */
function getObjectKeys(obj) {
const stack = [];
const keys: Set<string> = new Set();
@ -58,9 +58,9 @@ function getObjectKeys(obj) {
return keys;
}
/** 将展开的key缓存 */
/** Cache expanded keys */
const keysCache: Map<string, Set<string>> = new Map();
const flatI18n = (prefix = "zh-CN") => {
const flatI18n = (prefix = "vi") => {
let cache = keysCache.get(prefix);
if (!cache) {
cache = getObjectKeys(siphonI18n(prefix));
@ -70,16 +70,16 @@ const flatI18n = (prefix = "zh-CN") => {
};
/**
* locales文件夹下文件进行国际化匹配
* @param message message
* @returns message
* Internationalization transformation utility function
* @param message message to transform
* @returns transformed message
*/
export function transformI18n(message: any = "") {
if (!message) {
return "";
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
// Handle dynamic route titles stored in format {zh:"",en:""}
if (typeof message === "object") {
const locale: string | WritableComputedRef<string> | any =
i18n.global.locale;
@ -88,17 +88,17 @@ export function transformI18n(message: any = "") {
const key = message.match(/(\S*)\./)?.input;
if (key && flatI18n("zh-CN").has(key)) {
if (key && flatI18n("vi").has(key)) {
return i18n.global.t.call(i18n.global.locale, message);
} else if (!key && Object.hasOwn(siphonI18n("zh-CN"), message)) {
// 兼容非嵌套形式的国际化写法
} else if (!key && Object.hasOwn(siphonI18n("vi"), message)) {
// Compatible with non-nested internationalization format
return i18n.global.t.call(i18n.global.locale, message);
} else {
return message;
}
}
/** 此函数只是配合i18n Ally插件来进行国际化智能提示并无实际意义只对提示起作用如果不需要国际化可删除 */
/** This function is only for internationalization intelligent prompts with i18n Ally plugin (only affects prompts), can be removed if internationalization is not needed */
export const $t = (key: string) => key;
export const i18n: I18n = createI18n({
@ -106,7 +106,7 @@ export const i18n: I18n = createI18n({
locale:
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}locale`
)?.locale ?? "zh",
)?.locale ?? "vi",
fallbackLocale: "en",
messages: localesConfigs
});

103
src/plugins/vxeTable.ts Normal file
View File

@ -0,0 +1,103 @@
import "vxe-table/lib/style.css";
// import "xe-utils";
// import XEUtils from "xe-utils";
import type { App } from "vue";
//import { i18n } from "@/plugins/i18n";
//import en from "vxe-table/lib/locale/lang/en-US";
import {
// 全局对象
VXETable,
// 表格功能
// Filter,
// Edit,
// Menu,
// Export,
// Keyboard,
// Validator,
Custom,
// 可选组件
Icon,
Column,
Grid,
Pager,
Select,
// Colgroup,
// Tooltip,
// Toolbar,
// Form,
// FormItem,
// FormGather,
// Checkbox,
// CheckboxGroup,
// Radio,
// RadioGroup,
// RadioButton,
// Switch,
// Input,
// Optgroup,
// Option,
// Textarea,
// Button,
// Modal,
// List,
// Pulldown,
// 表格
Table
} from "vxe-table";
// 全局默认参数
VXETable.setConfig({
// i18n: (key, args) => {
// return unref(i18n.global.locale) === "zh"
// ? XEUtils.toFormatString(XEUtils.get(zh, key), args)
// : XEUtils.toFormatString(XEUtils.get(en, key), args);
// },
// translate(key) {
// const NAMESPACED = ["el.", "buttons."];
// if (key && NAMESPACED.findIndex(v => key.includes(v)) !== -1) {
// return i18n.global.t.call(i18n.global.locale, key);
// }
// return key;
// }
});
export function useVxeTable(app: App) {
// 表格功能
app
// .use(Filter)
// .use(Edit)
// .use(Menu)
// .use(Export)
// .use(Keyboard)
// .use(Validator)
.use(Custom)
// 可选组件
.use(Icon)
.use(Column)
.use(Grid)
.use(Pager)
.use(Select)
// .use(Colgroup)
// .use(Tooltip)
// .use(Toolbar)
// .use(Form)
// .use(FormItem)
// .use(FormGather)
// .use(Checkbox)
// .use(CheckboxGroup)
// .use(Radio)
// .use(RadioGroup)
// .use(RadioButton)
// .use(Switch)
// .use(Input)
// .use(Optgroup)
// .use(Option)
// .use(Textarea)
// .use(Button)
// .use(Modal)
// .use(List)
// .use(Pulldown)
// 安装表格
.use(Table);
}

View File

@ -26,7 +26,9 @@ const IFrame = () => import("@/layout/frame.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
// 动态路由
// Định nghĩa các hàm xử lý định tuyến động
// Lấy danh sách định tuyến từ backend
import { getAsyncRoutes } from "@/api/routes";
function handRank(routeInfo: any) {
@ -39,10 +41,10 @@ function handRank(routeInfo: any) {
: false;
}
/** 按照路由中meta下的rank等级升序来排序路由 */
/** Sắp xếp định tuyến theo thứ tự tăng dần dựa trên meta.rank trong route */
function ascending(arr: any[]) {
arr.forEach((v, index) => {
// 当rank不存在时根据顺序自动创建首页路由永远在第一位
// Khi không có rank, tự động tạo theo thứ tự, đảm bảo trang chủ luôn là trang đầu tiên
if (handRank(v)) v.meta.rank = index + 2;
});
return arr.sort(
@ -52,7 +54,7 @@ function ascending(arr: any[]) {
);
}
/** 过滤meta中showLink为false的菜单 */
/** Lọc các menu có meta.showLink === false */
function filterTree(data: RouteComponent[]) {
const newTree = cloneDeep(data).filter(
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
@ -63,7 +65,7 @@ function filterTree(data: RouteComponent[]) {
return newTree;
}
/** 过滤children长度为0的的目录当目录下没有菜单时会过滤此目录目录没有赋予roles权限当目录下只要有一个菜单有显示权限那么此目录就会显示 */
/** Lọc các menu con có độ dài === 0, các mục không có menu sẽ bị loại bỏ */
function filterChildrenTree(data: RouteComponent[]) {
const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0);
newTree.forEach(
@ -72,7 +74,7 @@ function filterChildrenTree(data: RouteComponent[]) {
return newTree;
}
/** 判断两个数组彼此是否存在相同值 */
/** Kiểm tra xem hai mảng có phần tử chung hay không */
function isOneOfArray(a: Array<string>, b: Array<string>) {
return Array.isArray(a) && Array.isArray(b)
? intersection(a, b).length > 0
@ -81,7 +83,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
: true;
}
/** 从localStorage里取出当前登录用户的角色roles过滤无权限的菜单 */
/** Lấy danh sách các vai trò roles của người dùng từ localStorage và lọc các menu không có quyền */
function filterNoPermissionTree(data: RouteComponent[]) {
const currentRoles =
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
@ -94,31 +96,31 @@ function filterNoPermissionTree(data: RouteComponent[]) {
return filterChildrenTree(newTree);
}
/** 通过指定 `key` 获取父级路径集合,默认 `key` 为 `path` */
/** Lấy danh sách đường dẫn cha dựa trên `key` đã chỉ định, mặc định `key` là `path` */
function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") {
// 深度遍历查找
// Duyệt sâu tìm kiếm
function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) {
const item = routes[i];
// 返回父级path
// Trả về danh sách path cha
if (item[key] === value) return parents;
// children不存在或为空则不递归
// Nếu không có children hoặc children rỗng thì không đệ quy
if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈
// Khi tìm thấy path thì đưa path hiện tại vào stack
parents.push(item.path);
if (dfs(item.children, value, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈
// Khi tìm kiếm sâu không tìm thấy, bỏ path hiện tại ra khỏi stack
parents.pop();
}
// 未找到时返回空数组
// Khi không tìm thấy thì trả về mảng rỗng
return [];
}
return dfs(routes, value, []);
}
/** 查找对应 `path` 的路由信息 */
/** Tìm thông tin định tuyến theo `path` đã chỉ định */
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
@ -149,14 +151,14 @@ function addPathMatch() {
}
}
/** 处理动态路由(后端返回的路由) */
/** Xử lý định tuyến động (các route từ backend) */
function handleAsyncRoutes(routeList) {
if (routeList.length === 0) {
usePermissionStoreHook().handleWholeMenus(routeList);
} else {
formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
(v: RouteRecordRaw) => {
// 防止重复添加路由
// Tránh thêm định tuyến trùng lặp
if (
router.options.routes[0].children.findIndex(
value => value.path === v.path
@ -164,9 +166,9 @@ function handleAsyncRoutes(routeList) {
) {
return;
} else {
// 切记将路由push到routes后还需要使用addRoute这样路由才能正常跳转
// Lưu ý sau khi thêm định tuyến vào routes cần sử dụng addRoute để định tuyến mới có thể hoạt động bình thường
router.options.routes[0].children.push(v);
// 最终路由进行升序
// Sắp xếp các định tuyến cuối cùng
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
const flattenRouters: any = router
@ -189,10 +191,10 @@ function handleAsyncRoutes(routeList) {
addPathMatch();
}
/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/
/** Khởi tạo định tuyến (`new Promise` để tránh vòng lặp vô hạn trong yêu cầu không đồng bộ) */
function initRouter() {
if (getConfig()?.CachingAsyncRoutes) {
// 开启动态路由缓存本地localStorage
// Bật cache định tuyến động vào localStorage
const key = "async-routes";
const asyncRouteList = storageLocal().getItem(key) as any;
if (asyncRouteList && asyncRouteList?.length > 0) {
@ -220,9 +222,9 @@ function initRouter() {
}
/**
*
* @param routesList
* @returns
* Chuyển đi mảng đnh tuyến nhiều cấp thành mảng một cấp
* @param routesList Mảng đnh tuyến đu vào
* @returns Trả về mảng đnh tuyến đã đưc xử
*/
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
if (routesList.length === 0) return routesList;
@ -238,10 +240,10 @@ function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
}
/**
* keep-alive
* Xử mảng đnh tuyến một cấp thành mảng nhiều cấp (Tất cả đnh tuyến từ cấp 3 trở lên đu đưc chuyển thành cấp 2, keep-alive chỉ hỗ trợ đến cấp 2)
* https://github.com/pure-admin/vue-pure-admin/issues/67
* @param routesList
* @returns
* @param routesList Mảng đnh tuyến menu đã đưc xử
* @returns Trả về mảng đnh tuyến đưc chuyển đi lại thành đnh dạng quy đnh
*/
function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
if (routesList.length === 0) return routesList;
@ -263,7 +265,7 @@ function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
return newRoutesList;
}
/** 处理缓存路由(添加、删除、刷新) */
/** Xử lý định tuyến cache (Thêm, xóa, làm mới) */
function handleAliveRoute({ name }: ToRouteType, mode?: string) {
switch (mode) {
case "add":
@ -298,23 +300,23 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) {
}
}
/** 过滤后端传来的动态路由 重新生成规范路由 */
/** Lọc các định tuyến động trả về từ backend, tạo lại định dạng chuẩn của định tuyến */
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
if (!arrRoutes || !arrRoutes.length) return;
const modulesRoutesKeys = Object.keys(modulesRoutes);
arrRoutes.forEach((v: RouteRecordRaw) => {
// 将backstage属性加入meta标识此路由为后端返回路由
// Thêm thuộc tính meta, đánh dấu đây là định tuyến trả về từ backend
v.meta.backstage = true;
// 父级的redirect属性取值如果子级存在且父级的redirect属性不存在默认取第一个子级的path如果子级存在且父级的redirect属性存在取存在的redirect属性会覆盖默认值
// Nếu có children và parent không có redirect, mặc định lấy path của children đầu tiên; Nếu có children và parent có redirect, lấy redirect của parent, sẽ ghi đè giá trị mặc định
if (v?.children && v.children.length && !v.redirect)
v.redirect = v.children[0].path;
// 父级的name属性取值如果子级存在且父级的name属性不存在默认取第一个子级的name如果子级存在且父级的name属性存在取存在的name属性会覆盖默认值注意测试中发现父级的name不能和子级name重复如果重复会造成重定向无效跳转404所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复)
// Nếu có children và parent không có name, mặc định lấy name của children đầu tiên; Nếu có children và parent có name, lấy name của parent, sẽ ghi đè giá trị mặc định (Chú ý: Trong thử nghiệm, name của parent không được trùng với name của children, nếu trùng sẽ dẫn đến không có redirect (chuyển hướng 404), vì vậy ở đây sẽ tự động thêm `Parent` vào cuối name của parent để tránh trùng)
if (v?.children && v.children.length && !v.name)
v.name = (v.children[0].name as string) + "Parent";
if (v.meta?.frameSrc) {
v.component = IFrame;
} else {
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会跟path保持一致
// Thích nghi với component và đường dẫn component không trả về từ backend (nếu backend trả về đường dẫn component, thì path có thể viết bất kỳ, nếu không trả về, đường dẫn component sẽ giống path)
const index = v?.component
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
@ -327,20 +329,20 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
return arrRoutes;
}
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
/** Lấy chế độ lịch sử định tuyến https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
function getHistoryMode(routerHistory): RouterHistory {
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
// Nếu len = 1 chỉ có lịch sử, len = 2 có tham số base trong lịch sử https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
const leftMode = historyMode[0];
const rightMode = historyMode[1];
// no param
// Không có tham số
if (historyMode.length === 1) {
if (leftMode === "hash") {
return createWebHashHistory("");
} else if (leftMode === "h5") {
return createWebHistory("");
}
} //has param
} // Có tham số
else if (historyMode.length === 2) {
if (leftMode === "hash") {
return createWebHashHistory(rightMode);
@ -350,15 +352,15 @@ function getHistoryMode(routerHistory): RouterHistory {
}
}
/** 获取当前页面按钮级别的权限 */
/** Lấy quyền của nút ở trang hiện tại */
function getAuths(): Array<string> {
return router.currentRoute.value.meta.auths as Array<string>;
}
/** 是否有按钮级别的权限 */
/** Kiểm tra quyền của nút */
function hasAuth(value: string | Array<string>): boolean {
if (!value) return false;
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
/** Lấy tất cả các giá trị `code` tuỳ chỉnh từ trường `meta` của route hiện tại */
const metaAuths = getAuths();
if (!metaAuths) return false;
const isAuths = isString(value)
@ -379,7 +381,7 @@ function handleTopMenu(route) {
}
}
/** 获取所有菜单中的第一个菜单(顶级菜单)*/
/** Lấy menu đầu tiên (menu cấp đỉnh) từ tất cả các menu */
function getTopMenu(tag = false): menuType {
const topMenu = handleTopMenu(
usePermissionStoreHook().wholeMenus[0]?.children[0]

View File

@ -19,13 +19,13 @@ export const useAppStore = defineStore({
withoutAnimation: false,
isClickCollapse: false
},
// 这里的layout用于监听容器拖拉后恢复对应的导航模式
// Layout này được sử dụng để lắng nghe khi container kéo thả và phục hồi chế độ điều hướng tương ứng
layout:
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
)?.layout ?? getConfig().Layout,
device: deviceDetection() ? "mobile" : "desktop",
// 浏览器窗口的可视区域大小
// Kích thước vùng hiển thị của cửa sổ trình duyệt
viewportSize: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight

View File

@ -23,14 +23,15 @@ html {
body {
width: 100%;
height: 100%;
margin: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
line-height: inherit;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizelegibility;
width: 100%;
height: 100%;
margin: 0;
font-family: "Segoe UI", Arial, sans-serif;
line-height: inherit;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizelegibility;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
hr {

View File

@ -2,60 +2,62 @@ import Cookies from "js-cookie";
import { storageLocal } from "@pureadmin/utils";
import { useUserStoreHook } from "@/store/modules/user";
// Định nghĩa interface cho thông tin dữ liệu người dùng
export interface DataInfo<T> {
/** token */
/** Token truy cập */
accessToken: string;
/** `accessToken`的过期时间(时间戳) */
/** Thời gian hết hạn của accessToken (dưới dạng timestamp) */
expires: T;
/** 用于调用刷新accessToken的接口时所需的token */
/** Token dùng để làm mới accessToken */
refreshToken: string;
/** 头像 */
/** Ảnh đại diện */
avatar?: string;
/** 用户名 */
/** Tên đăng nhập */
username?: string;
/** 昵称 */
/** Biệt danh */
nickname?: string;
/** 当前登录用户的角色 */
/** Các vai trò của người dùng hiện tại */
roles?: Array<string>;
}
export const userKey = "user-info";
export const TokenKey = "authorized-token";
/**
* `multiple-tabs``cookie`
*
* `multiple-tabs``cookie`
*
* */
export const multipleTabsKey = "multiple-tabs";
// Khai báo các hằng số
export const userKey = "user-info"; // Key cho localStorage lưu thông tin người dùng
export const TokenKey = "authorized-token"; // Key cho cookie lưu token truy cập
export const multipleTabsKey = "multiple-tabs"; // Key cho cookie kiểm tra nhiều tab mở
/** 获取`token` */
/**
* Hàm lấy thông tin token từ cookie hoặc localStorage
* Nếu không tìm thấy token trong cookie, thử lấy từ localStorage
*/
export function getToken(): DataInfo<number> {
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
return Cookies.get(TokenKey)
? JSON.parse(Cookies.get(TokenKey))
: storageLocal().getItem(userKey);
}
/**
* @description `token``token`
* `accessToken`访使`token``refreshToken``accessToken``token``refreshToken`30`accessToken`2`expires``accessToken`
* `accessToken``expires``refreshToken`key值为authorized-token的cookie里
* `avatar``username``nickname``roles``refreshToken``expires`key值为`user-info`localStorage里`multipleTabsKey`
* Hàm thiết lập thông tin token thông tin người dùng
* Sử dụng phương thức làm mới token không cần đăng nhập lại
* Lưu accessToken, expires refreshToken vào cookie với key TokenKey
* Lưu avatar, username, nickname, roles, refreshToken expires vào localStorage với key userKey
*/
export function setToken(data: DataInfo<Date>) {
let expires = 0;
const { accessToken, refreshToken } = data;
const { isRemembered, loginDay } = useUserStoreHook();
expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳将此处代码改为expires = data.expires然后把上面的DataInfo<Date>改成DataInfo<number>即可
// Xử lý giá trị của expires
expires = new Date(data.expires).getTime();
const cookieString = JSON.stringify({ accessToken, expires, refreshToken });
// Thiết lập cookie TokenKey với các thông tin vừa xử lý
expires > 0
? Cookies.set(TokenKey, cookieString, {
expires: (expires - Date.now()) / 86400000
})
: Cookies.set(TokenKey, cookieString);
// Thiết lập cookie multipleTabsKey để kiểm tra nhiều tab
Cookies.set(
multipleTabsKey,
"true",
@ -66,6 +68,7 @@ export function setToken(data: DataInfo<Date>) {
: {}
);
// Hàm để thiết lập thông tin người dùng vào localStorage và store Vuex
function setUserKey({ avatar, username, nickname, roles }) {
useUserStoreHook().SET_AVATAR(avatar);
useUserStoreHook().SET_USERNAME(username);
@ -81,6 +84,7 @@ export function setToken(data: DataInfo<Date>) {
});
}
// Kiểm tra và thiết lập thông tin người dùng
if (data.username && data.roles) {
const { username, roles } = data;
setUserKey({
@ -107,14 +111,18 @@ export function setToken(data: DataInfo<Date>) {
}
}
/** 删除`token`以及key值为`user-info`的localStorage信息 */
/**
* Hàm xóa thông tin token thông tin người dùng từ cookie localStorage
*/
export function removeToken() {
Cookies.remove(TokenKey);
Cookies.remove(multipleTabsKey);
storageLocal().removeItem(userKey);
Cookies.remove(TokenKey); // Xóa cookie TokenKey
Cookies.remove(multipleTabsKey); // Xóa cookie multipleTabsKey
storageLocal().removeItem(userKey); // Xóa localStorage userKey
}
/** 格式化tokenjwt格式 */
/**
* Hàm đnh dạng token JWT
*/
export const formatToken = (token: string): string => {
return "Bearer " + token;
};

View File

@ -1,5 +1,4 @@
// 如果项目出现 `global is not defined` 报错,可能是您引入某个库的问题,比如 aws-sdk-js https://github.com/aws/aws-sdk-js
// 解决办法就是将该文件引入 src/main.ts 即可 import "@/utils/globalPolyfills";
// Nếu biến `global` chưa được định nghĩa, gán `window` cho `global`
if (typeof (window as any).global === "undefined") {
(window as any).global = window;
}

View File

@ -1,3 +1,4 @@
// Axios instance and related configurations
import Axios, {
type AxiosInstance,
type AxiosRequestConfig,
@ -14,16 +15,16 @@ import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
// For more configurations, please refer to: www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
// 请求超时时间
// Request timeout
timeout: 10000,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
// 数组格式参数序列化https://github.com/axios/axios/issues/5142
// Array format parameter serialization (https://github.com/axios/axios/issues/5142)
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer
}
@ -35,19 +36,19 @@ class PureHttp {
this.httpInterceptorsResponse();
}
/** `token`过期后,暂存待执行的请求 */
/** Queue for storing requests while token is refreshing */
private static requests = [];
/** 防止重复刷新`token` */
/** Prevents duplicate token refresh */
private static isRefreshing = false;
/** 初始化配置对象 */
/** Initial configuration object */
private static initConfig: PureHttpRequestConfig = {};
/** 保存当前`Axios`实例对象 */
/** Stores the current Axios instance */
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
/** 重连原始请求 */
/** Retry the original request */
private static retryOriginalRequest(config: PureHttpRequestConfig) {
return new Promise(resolve => {
PureHttp.requests.push((token: string) => {
@ -57,13 +58,13 @@ class PureHttp {
});
}
/** 请求拦截 */
/** Request interceptor */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
// Start progress bar animation
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
// Prioritize checking if post/get methods have a callback, otherwise execute initialization settings callback, etc.
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
return config;
@ -72,7 +73,7 @@ class PureHttp {
PureHttp.initConfig.beforeRequestCallback(config);
return config;
}
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
/** Request whitelist, for endpoints that do not require token (prevent token expiration causing infinite loop issue by setting request whitelist) */
const whiteList = ["/refresh-token", "/login"];
return whiteList.some(url => config.url.endsWith(url))
? config
@ -84,7 +85,7 @@ class PureHttp {
if (expired) {
if (!PureHttp.isRefreshing) {
PureHttp.isRefreshing = true;
// token过期刷新
// Refresh token when expired
useUserStoreHook()
.handRefreshToken({ refreshToken: data.refreshToken })
.then(res => {
@ -115,15 +116,15 @@ class PureHttp {
);
}
/** 响应拦截 */
/** Response interceptor */
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
// End progress bar animation
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
// Prioritize checking if post/get methods have a callback, otherwise execute initialization settings callback, etc.
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
@ -137,15 +138,15 @@ class PureHttp {
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
// End progress bar animation
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
// All response errors distinguish between canceled requests and non-canceled requests
return Promise.reject($error);
}
);
}
/** 通用请求工具函数 */
/** Common request utility function */
public request<T>(
method: RequestMethods,
url: string,
@ -159,7 +160,7 @@ class PureHttp {
...axiosConfig
} as PureHttpRequestConfig;
// 单独处理自定义请求/响应回调
// Handle custom request/response callbacks separately
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)
@ -172,7 +173,7 @@ class PureHttp {
});
}
/** 单独抽离的`post`工具函数 */
/** Separated 'post' utility function */
public post<T, P>(
url: string,
params?: AxiosRequestConfig<P>,
@ -181,7 +182,7 @@ class PureHttp {
return this.request<T>("post", url, params, config);
}
/** 单独抽离的`get`工具函数 */
/** Separated 'get' utility function */
public get<T, P>(
url: string,
params?: AxiosRequestConfig<P>,

View File

@ -6,17 +6,17 @@ class StorageProxy implements ProxyStorage {
constructor(storageModel) {
this.storage = storageModel;
this.storage.config({
// 首选IndexedDB作为第一驱动不支持IndexedDB会自动降级到localStorageWebSQL被弃用详情看https://developer.chrome.com/blog/deprecating-web-sql
// Ưu tiên sử dụng IndexedDB làm driver chính, nếu không hỗ trợ IndexedDB sẽ tự động hạ cấp xuống localStorage (WebSQL đã bị loại bỏ, xem chi tiết tại https://developer.chrome.com/blog/deprecating-web-sql)
driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE],
name: "pure-admin"
});
}
/**
* @description 线
* @param k
* @param v
* @param m ```0`
* @description Lưu dữ liệu với tên khóa tương ng vào kho lưu trữ ngoại tuyến
* @param k Tên khóa
* @param v Giá trị
* @param m Thời gian lưu trữ (đơn vị phút, mặc đnh 0 phút, lưu trữ vĩnh viễn)
*/
public async setItem<T>(k: string, v: T, m = 0): Promise<T> {
return new Promise((resolve, reject) => {
@ -35,8 +35,8 @@ class StorageProxy implements ProxyStorage {
}
/**
* @description 线
* @param k
* @description Lấy giá trị tương ng với tên khóa từ kho lưu trữ ngoại tuyến
* @param k Tên khóa
*/
public async getItem<T>(k: string): Promise<T> {
return new Promise((resolve, reject) => {
@ -54,8 +54,8 @@ class StorageProxy implements ProxyStorage {
}
/**
* @description 线
* @param k
* @description Xóa giá trị tương ng với tên khóa từ kho lưu trữ ngoại tuyến
* @param k Tên khóa
*/
public async removeItem(k: string) {
return new Promise<void>((resolve, reject) => {
@ -71,7 +71,7 @@ class StorageProxy implements ProxyStorage {
}
/**
* @description 线
* @description Xóa tất cả các tên khóa từ kho lưu trữ ngoại tuyến, đt lại sở dữ liệu
*/
public async clear() {
return new Promise<void>((resolve, reject) => {
@ -87,7 +87,7 @@ class StorageProxy implements ProxyStorage {
}
/**
* @description key
* @description Lấy tất cả các khóa từ kho dữ liệu
*/
public async keys() {
return new Promise<string[]>((resolve, reject) => {
@ -104,6 +104,6 @@ class StorageProxy implements ProxyStorage {
}
/**
* [localforage](https://localforage.docschina.org/) 支持设置过期时间,提供完整的类型提示
* Bao bọc lại [localforage](https://localforage.docschina.org/) hỗ trợ thiết lập thời gian hết hạn, cung cấp gợi ý kiểu dữ liệu đầy đủ
*/
export const localForage = () => new StorageProxy(forage);

View File

@ -6,34 +6,34 @@ type messageStyle = "el" | "antd";
type messageTypes = "info" | "success" | "warning" | "error";
interface MessageParams {
/** 消息类型,可选 `info` 、`success` 、`warning` 、`error` ,默认 `info` */
/** Loại thông báo, có thể là `info`, `success`, `warning`, `error`. Mặc định là `info` */
type?: messageTypes;
/** 自定义图标,该属性会覆盖 `type` 的图标 */
/** Biểu tượng tùy chỉnh, thuộc tính này sẽ ghi đè biểu tượng của `type` */
icon?: any;
/** 是否将 `message` 属性作为 `HTML` 片段处理,默认 `false` */
/** Có sử dụng `message` như là một đoạn mã HTML hay không, mặc định là `false` */
dangerouslyUseHTMLString?: boolean;
/** 消息风格,可选 `el` 、`antd` ,默认 `antd` */
/** Kiểu giao diện của thông báo, có thể là `el` hoặc `antd`, mặc định là `antd` */
customClass?: messageStyle;
/** 显示时间,单位为毫秒。设为 `0` 则不会自动关闭,`element-plus` 默认是 `3000` ,平台改成默认 `2000` */
/** Thời gian hiển thị, tính bằng mili giây. Đặt là `0` thì không tự động đóng, `element-plus` mặc định là `3000`, nền tảng đã đổi thành mặc định `2000` */
duration?: number;
/** 是否显示关闭按钮,默认值 `false` */
/** Hiển thị nút đóng, mặc định là `false` */
showClose?: boolean;
/** 文字是否居中,默认值 `false` */
/** Văn bản có căn giữa hay không, mặc định là `false` */
center?: boolean;
/** `Message` 距离窗口顶部的偏移量,默认 `20` */
/** Độ lệch của `Message` so với đỉnh cửa sổ, mặc định là `20` */
offset?: number;
/** 设置组件的根元素,默认 `document.body` */
/** Thiết lập phần tử gốc của thành phần, mặc định là `document.body` */
appendTo?: string | HTMLElement;
/** 合并内容相同的消息,不支持 `VNode` 类型的消息,默认值 `false` */
/** Gộp các thông báo cùng nội dung giống nhau, không hỗ trợ loại `VNode`, mặc định là `false` */
grouping?: boolean;
/** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
/** Callback khi đóng thông báo, tham số là instance `message` đã đóng */
onClose?: Function | null;
}
/** 用法非常简单,参考 src/views/components/message/index.vue 文件 */
/** Sử dụng rất đơn giản, xem tệp src/views/components/message/index.vue để biết thêm chi tiết */
/**
* `Message`
* Hàm thông báo `Message`
*/
const message = (
message: string | VNode | (() => VNode),
@ -70,7 +70,7 @@ const message = (
offset,
appendTo,
grouping,
// 全局搜 pure-message 即可知道该类的样式位置
// Tìm kiếm toàn cầu pure-message để biết vị trí lớp này
customClass: customClass === "antd" ? "pure-message" : "",
onClose: () => (isFunction(onClose) ? onClose() : null)
});
@ -78,7 +78,7 @@ const message = (
};
/**
* `Message`
* Đóng tất cả các thông báo `Message`
*/
const closeAllMessage = (): void => ElMessage.closeAll();

View File

@ -1,13 +1,20 @@
import type { Emitter } from "mitt";
import mitt from "mitt";
/** 全局公共事件需要在此处添加类型 */
/** Các sự kiện công cộng toàn cầu cần thêm các loại ở đây*/
type Events = {
openPanel: string;
tagViewsChange: string;
tagViewsShowModel: string;
logoChange: boolean;
changLayoutRoute: string;
imageInfo: {
img: HTMLImageElement;
height: number;
width: number;
x: number;
y: number;
};
};
export const emitter: Emitter<Events> = mitt<Events>();

View File

@ -1,25 +1,25 @@
import { useEventListener } from "@vueuse/core";
/** 是否为`img`标签 */
/** Kiểm tra xem có phải là thẻ `img` */
function isImgElement(element) {
return typeof HTMLImageElement !== "undefined"
? element instanceof HTMLImageElement
: element.tagName.toLowerCase() === "img";
}
// 在 src/main.ts 引入并调用即可 import { addPreventDefault } from "@/utils/preventDefault"; addPreventDefault();
// Import và gọi từ src/main.ts import { addPreventDefault } from "@/utils/preventDefault"; addPreventDefault();
export const addPreventDefault = () => {
// 阻止通过键盘F12快捷键打开浏览器开发者工具面板
// Ngăn chặn mở bảng công cụ phát triển của trình duyệt bằng phím tắt F12
useEventListener(
window.document,
"keydown",
ev => ev.key === "F12" && ev.preventDefault()
);
// 阻止浏览器默认的右键菜单弹出(不会影响自定义右键事件)
// Ngăn chặn menu chuột phải mặc định của trình duyệt (không ảnh hưởng đến sự kiện chuột phải tùy chỉnh)
useEventListener(window.document, "contextmenu", ev => ev.preventDefault());
// 阻止页面元素选中
// Ngăn chặn các phần tử trang được chọn
useEventListener(window.document, "selectstart", ev => ev.preventDefault());
// 浏览器中图片通常默认是可拖动的,并且可以在新标签页或窗口中打开,或者将其拖动到其他应用程序中,此处将其禁用,使其默认不可拖动
// Trình duyệt thường cho phép kéo và thả hình ảnh mặc định và có thể mở trong tab hoặc cửa sổ mới, hoặc kéo nó vào ứng dụng khác. Ở đây, chúng tôi vô hiệu hóa nó để mặc định hình ảnh không thể kéo và thả
useEventListener(
window.document,
"dragstart",

View File

@ -2,15 +2,15 @@ import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({
// 动画方式
// Animation mode
easing: "ease",
// 递增进度条的速度
// Tăng tốc độ của thanh tiến trình
speed: 500,
// 是否显示加载ico
// Có hiển thị ico đang tải hay không
showSpinner: false,
// 自动递增间隔
// khoảng thời gian tự động tăng
trickleSpeed: 200,
// 初始化时的最小百分比
// Tỷ lệ phần trăm tối thiểu khi khởi tạo
minimum: 0.3
});

View File

@ -22,7 +22,7 @@ const newPropTypes = createTypes({
integer: undefined
}) as PropTypes;
// 从 vue-types v5.0 开始extend()方法已经废弃当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
// Bắt đầu từ vue-types v5.0, phương thức Extend() đã bị bỏ và được thay đổi thành phương thức ES6+ được đề xuất chính thức https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html #phương thức -extend
export default class propTypes extends newPropTypes {
// a native-like validator that supports the `.validable` method
static get style() {

View File

@ -1,4 +1,4 @@
// 响应式storage
// Storage phản ứng
import type { App } from "vue";
import Storage from "responsive-storage";
import { routerArrays } from "@/layout/types";
@ -8,35 +8,35 @@ export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
const nameSpace = responsiveStorageNameSpace();
const configObj = Object.assign(
{
// 国际化 默认中文zh
// Quốc tế hóa, mặc định là tiếng Việt vn
locale: Storage.getData("locale", nameSpace) ?? {
locale: config.Locale ?? "zh"
locale: config.Locale ?? "vi" // Đặt mặc định là "vi" nếu không có dữ liệu
},
// layout模式以及主题
// Mẫu bố trí và chủ đề
layout: Storage.getData("layout", nameSpace) ?? {
layout: config.Layout ?? "vertical",
theme: config.Theme ?? "light",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "#409EFF",
themeColor: config.Theme ?? "light", // 主题色对应系统配置中的主题色与theme不同的是它不会受到浅色、深色整体风格切换的影响只会在手动点击主题色时改变
overallStyle: config.OverallStyle ?? "light" // 整体风格浅色light、深色dark、自动system
layout: config.Layout ?? "vertical", // Bố cục mặc định là "vertical" nếu không có dữ liệu
theme: config.Theme ?? "light", // Chủ đề mặc định là "light" nếu không có dữ liệu
darkMode: config.DarkMode ?? false, // Chế độ tối mặc định là false nếu không có dữ liệu
sidebarStatus: config.SidebarStatus ?? true, // Trạng thái thanh bên mặc định là true nếu không có dữ liệu
epThemeColor: config.EpThemeColor ?? "#409EFF", // Màu chủ đề EP mặc định là "#409EFF" nếu không có dữ liệu
themeColor: config.Theme ?? "light", // Màu chủ đề (tương ứng với màu chủ đề trong cấu hình hệ thống, khác với chủ đề là nó sẽ không bị ảnh hưởng bởi việc chuyển đổi phong cách toàn cầu từ sáng sang tối, chỉ thay đổi khi người dùng nhấp vào màu chủ đề)
overallStyle: config.OverallStyle ?? "light" // Phong cách toàn cầu (Sáng: light, Tối: dark, Tự động: system)
},
// 系统配置-界面显示
// Cấu hình hệ thống - Hiển thị giao diện
configure: Storage.getData("configure", nameSpace) ?? {
grey: config.Grey ?? false,
weak: config.Weak ?? false,
hideTabs: config.HideTabs ?? false,
hideFooter: config.HideFooter ?? true,
showLogo: config.ShowLogo ?? true,
showModel: config.ShowModel ?? "smart",
multiTagsCache: config.MultiTagsCache ?? false,
stretch: config.Stretch ?? false
grey: config.Grey ?? false, // Xám mờ mặc định là false nếu không có dữ liệu
weak: config.Weak ?? false, // Yếu mặc định là false nếu không có dữ liệu
hideTabs: config.HideTabs ?? false, // Ẩn tab mặc định là false nếu không có dữ liệu
hideFooter: config.HideFooter ?? true, // Ẩn chân trang mặc định là true nếu không có dữ liệu
showLogo: config.ShowLogo ?? true, // Hiển thị logo mặc định là true nếu không có dữ liệu
showModel: config.ShowModel ?? "chrome", // Hiển thị mô hình mặc định là "chrome" nếu không có dữ liệu
multiTagsCache: config.MultiTagsCache ?? false, // Bộ nhớ cache nhiều thẻ mặc định là false nếu không có dữ liệu
stretch: config.Stretch ?? false // Kéo dài mặc định là false nếu không có dữ liệu
}
},
config.MultiTagsCache
? {
// 默认显示顶级菜单tag
// Hiển thị thẻ menu cấp độ cao nhất mặc định
tags: Storage.getData("tags", nameSpace) ?? routerArrays
}
: {}

View File

@ -2,22 +2,22 @@ import { removeToken, setToken, type DataInfo } from "./auth";
import { subBefore, getQueryMap } from "@pureadmin/utils";
/**
* http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
*
*
* 1.
* 2.url中的重要参数信息 setToken
* 3. url
* 4.使 window.location.replace
* Đăng nhập đơn giản bằng SSO phía frontend, tuỳ chỉnh theo yêu cầu thực tế của doanh nghiệp, sau khi nền tảng khởi đng, bạn thể kiểm tra thử nghiệm đa phương bằng cách truy cập vào liên kết sau: http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
* Chú ý:
* Kiểm tra xem phải đăng nhập đơn giản không, nếu không phải thì trả về không thực hiện bất kỳ xử logic nào, dưới đây xử logic sau khi đăng nhập đơn giản
* 1. Xóa thông tin trên thiết bị đa phương;
* 2. Lấy thông tin tham số quan trọng từ URL, sau đó lưu trữ bằng setToken vào thiết bị đa phương;
* 3. Xóa các tham số không cần thiết đ hiển thị trong URL;
* 4. Sử dụng window.location.replace đ chuyển hướng đến trang chính xác.
*/
(function () {
// 获取 url 中的参数
// Lấy các tham số từ URL
const params = getQueryMap(location.href) as DataInfo<Date>;
const must = ["username", "roles", "accessToken"];
const mustLength = must.length;
if (Object.keys(params).length !== mustLength) return;
// url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环
// Nếu các tham số URL đủ điều kiện trong must, đánh dấu là đăng nhập đơn giản, tránh vòng lặp vô hạn khi làm mới trang không phải đăng nhập đơn giản
let sso = [];
let start = 0;
@ -31,15 +31,15 @@ import { subBefore, getQueryMap } from "@pureadmin/utils";
}
if (sso.length === mustLength) {
// 判定为单点登录
// Đánh dấu là đăng nhập đơn giản
// 清空本地旧信息
// Xóa thông tin cũ trên thiết bị client
removeToken();
// 保存新信息到本地
// Lưu trữ thông tin mới vào thiết bị client
setToken(params);
// 删除不需要显示在 url 的参数
// Xóa các tham số không cần thiết để hiển thị trong URL
delete params.roles;
delete params.accessToken;
@ -51,7 +51,7 @@ import { subBefore, getQueryMap } from "@pureadmin/utils";
.replace(/:/g, "=")
.replace(/,/g, "&")}`;
// 替换历史记录项
// Thay thế mục lịch sử
window.location.replace(newUrl);
} else {
return;

View File

@ -1,7 +1,7 @@
/**
* @description uniqueId
* @param tree
* @returns uniqueId组成的数组
* @description Trích xuất mỗi mục uniqueId từ cây menu
* @param tree Cây menu
* @returns Mảng chứa mỗi mục uniqueId
*/
export const extractPathList = (tree: any[]): any => {
if (!Array.isArray(tree)) {
@ -21,10 +21,10 @@ export const extractPathList = (tree: any[]): any => {
};
/**
* @description children的length为1children并自动组建唯一uniqueId
* @param tree
* @param pathList id组成的数组
* @returns uniqueId后的树
* @description Nếu số lượng children của parent 1, xóa children tự đng xây dựng uniqueId duy nhất
* @param tree Cây menu
* @param pathList Mảng chứa mỗi mục id
* @returns Cây với uniqueId duy nhất đã đưc xây dựng
*/
export const deleteChildren = (tree: any[], pathList = []): any => {
if (!Array.isArray(tree)) {
@ -48,10 +48,10 @@ export const deleteChildren = (tree: any[], pathList = []): any => {
};
/**
* @description
* @param tree
* @param pathList id组成的数组
* @returns
* @description Xây dựng cấu trúc cây
* @param tree Cây menu
* @param pathList Mảng chứa mỗi mục id
* @returns Cây sau khi đã xây dựng cấu trúc cây
*/
export const buildHierarchyTree = (tree: any[], pathList = []): any => {
if (!Array.isArray(tree)) {
@ -72,10 +72,10 @@ export const buildHierarchyTree = (tree: any[], pathList = []): any => {
};
/**
* @description 广uniqueId找当前节点信息
* @param tree
* @param uniqueId uniqueId
* @returns
* @description Duyệt cây theo chiều rộng, tìm thông tin của node hiện tại dựa trên uniqueId
* @param tree Cây menu
* @param uniqueId uniqueId
* @returns Thông tin của node hiện tại
*/
export const getNodeByUniqueId = (
tree: any[],
@ -96,11 +96,11 @@ export const getNodeByUniqueId = (
};
/**
* @description uniqueId节点中追加字段
* @param tree
* @param uniqueId uniqueId
* @param fields
* @returns
* @description Thêm trường vào node hiện tại dựa trên uniqueId
* @param tree Cây menu
* @param uniqueId uniqueId
* @param fields Các trường cần thêm
* @returns Cây sau khi đã thêm trường
*/
export const appendFieldByUniqueId = (
tree: any[],
@ -127,12 +127,12 @@ export const appendFieldByUniqueId = (
};
/**
* @description
* @param data
* @param id id字段 id
* @param parentId parentId
* @param children children
* @returns
* @description Xây dựng cấu trúc dữ liệu cây
* @param data Dữ liệu nguồn
* @param id Trường id, mặc đnh "id"
* @param parentId Trường parentId, mặc đnh "parentId"
* @param children Trường children, mặc đnh "children"
* @returns Cây sau khi đã xây dựng cấu trúc dữ liệu
*/
export const handleTree = (
data: any[],

View File

@ -53,7 +53,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
.loginByUsername({ username: ruleForm.username, password: "admin123" })
.then(res => {
if (res.success) {
//
// Fetch backend routes
return initRouter().then(() => {
router.push(getTopMenu(true).path).then(() => {
message(t("login.pureLoginSuccess"), { type: "success" });
@ -68,7 +68,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
});
};
/** 使用公共函数,避免`removeEventListener`失效 */
/** Use common function to prevent `removeEventListener` failure */
function onkeypress({ code }: KeyboardEvent) {
if (["Enter", "NumpadEnter"].includes(code)) {
onLogin(ruleFormRef.value);
@ -88,7 +88,7 @@ onBeforeUnmount(() => {
<div class="select-none">
<img :src="bg" class="wave" />
<div class="flex-c absolute right-5 top-3">
<!-- 主题 -->
<!-- Theme -->
<el-switch
v-model="dataTheme"
inline-prompt
@ -96,7 +96,7 @@ onBeforeUnmount(() => {
:inactive-icon="darkIcon"
@change="dataThemeChange"
/>
<!-- 国际化 -->
<!-- Globalization -->
<el-dropdown trigger="click">
<globalization
class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300"
@ -104,16 +104,16 @@ onBeforeUnmount(() => {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:style="getDropdownItemStyle(locale, 'vi')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
@click="translationCh"
>
<IconifyIconOffline
v-show="locale === 'zh'"
class="check-zh"
v-show="locale === 'vi'"
class="check-vi"
:icon="Check"
/>
简体中文
Tiếng Việt
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@ -210,7 +210,7 @@ onBeforeUnmount(() => {
padding: 5px 40px;
}
.check-zh {
.check-vi {
position: absolute;
left: 20px;
}

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { useDark, useECharts } from "@pureadmin/utils";
import { type PropType, ref, computed, watch, nextTick } from "vue";
const props = defineProps({
requireData: {
type: Array as PropType<Array<number>>,
default: () => []
},
questionData: {
type: Array as PropType<Array<number>>,
default: () => []
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme
});
watch(
() => props,
async () => {
await nextTick(); // Đm bo cp nht DOM hoàn tt trưc khi thc thi
setOptions({
container: ".bar-card",
color: ["#41b6ff", "#e85f33"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "none"
}
},
grid: {
top: "20px",
left: "50px",
right: 0
},
legend: {
data: ["Số người cần thiết", "Số lượng các câu hỏi"],
textStyle: {
color: "#606266",
fontSize: "0.875rem"
},
bottom: 0
},
xAxis: [
{
type: "category",
data: ["T2", "T3", "T4", "T5", "T6", "T7", "CN"],
axisLabel: {
fontSize: "0.875rem"
},
axisPointer: {
type: "shadow"
}
}
],
yAxis: [
{
type: "value",
axisLabel: {
fontSize: "0.875rem"
},
splitLine: {
show: false // 线
}
// name: ": "
}
],
series: [
{
name: "Số người cần thiết",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#41b6ff",
borderRadius: [10, 10, 0, 0]
},
data: props.requireData
},
{
name: "Số lượng các câu hỏi",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#e86033ce",
borderRadius: [10, 10, 0, 0]
},
data: props.questionData
}
],
textStyle: {
fontFamily: '"Segoe UI", Arial, sans-serif;'
}
});
},
{
deep: true,
immediate: true
}
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 365px" />
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { type PropType, ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const props = defineProps({
data: {
type: Array as PropType<Array<number>>,
default: () => []
},
color: {
type: String,
default: "#41b6ff"
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
xAxis: {
type: "category",
show: false,
data: props.data
},
grid: {
top: "15px",
bottom: 0,
left: 0,
right: 0
},
yAxis: {
show: false,
type: "value"
},
series: [
{
data: props.data,
type: "line",
symbol: "none",
smooth: true,
color: props.color,
lineStyle: {
shadowOffsetY: 3,
shadowBlur: 7,
shadowColor: props.color
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
title: {
text: "100%",
left: "47%",
top: "30%",
textAlign: "center",
textStyle: {
fontSize: "16",
fontWeight: 600
}
},
polar: {
radius: ["100%", "90%"],
center: ["50%", "50%"]
},
angleAxis: {
max: 100,
show: false
},
radiusAxis: {
type: "category",
show: true,
axisLabel: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
series: [
{
type: "bar",
roundCap: true,
barWidth: 2,
showBackground: true,
backgroundStyle: {
color: "#dfe7ef"
},
data: [100],
coordinateSystem: "polar",
color: "#7846e5",
itemStyle: {
shadowBlur: 2,
shadowColor: "#7846e5",
shadowOffsetX: 0,
shadowOffsetY: 0
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -0,0 +1,3 @@
export { default as ChartBar } from "./ChartBar.vue";
export { default as ChartLine } from "./ChartLine.vue";
export { default as ChartRound } from "./ChartRound.vue";

View File

@ -0,0 +1,104 @@
import { tableData } from "../../data";
import { delay } from "@pureadmin/utils";
import { ref, onMounted, reactive } from "vue";
import type { PaginationProps } from "@pureadmin/table";
import ThumbUp from "@iconify-icons/ri/thumb-up-line";
import Hearts from "@iconify-icons/ri/hearts-line";
import Empty from "./empty.svg?component";
export function useColumns() {
const dataList = ref([]);
const loading = ref(true);
const columns: TableColumnList = [
{
sortable: true,
label: "Số thứ tự",
prop: "id"
},
{
sortable: true,
label: "Số người yêu cầu",
prop: "requiredNumber",
filterMultiple: false,
filterClassName: "pure-table-filter",
filters: [
{ text: "≥16000", value: "more" },
{ text: "<16000", value: "less" }
],
filterMethod: (value, { requiredNumber }) => {
return value === "more"
? requiredNumber >= 16000
: requiredNumber < 16000;
}
},
{
sortable: true,
label: "Số lượng câu hỏi",
prop: "questionNumber"
},
{
sortable: true,
label: "Số lượng đã giải quyết",
prop: "resolveNumber"
},
{
sortable: true,
label: "Mức độ hài lòng của người dùng",
minWidth: 100,
prop: "satisfaction",
cellRenderer: ({ row }) => (
<div class="flex justify-center w-full">
<span class="flex items-center w-[60px]">
<span class="ml-auto mr-2">{row.satisfaction}%</span>
<iconifyIconOffline
icon={row.satisfaction > 98 ? Hearts : ThumbUp}
color="#e85f33"
/>
</span>
</div>
)
},
{
sortable: true,
label: "Ngày thống kê",
prop: "date"
},
{
label: "Thao tác",
fixed: "right",
slot: "operation"
}
];
/** Cấu hình phân trang */
const pagination = reactive<PaginationProps>({
pageSize: 10,
currentPage: 1,
layout: "prev, pager, next",
total: 0,
align: "center"
});
function onCurrentChange(page: number) {
console.log("onCurrentChange", page);
loading.value = true;
delay(300).then(() => {
loading.value = false;
});
}
onMounted(() => {
dataList.value = tableData;
pagination.total = dataList.value.length;
loading.value = false;
});
return {
Empty,
loading,
columns,
dataList,
pagination,
onCurrentChange
};
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" class="empty-icon" viewBox="0 0 1024 1024"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18M673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4zm146.5-486.1c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8m-444.5 51.6L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4M512 239.3h2.5c19.5.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { useColumns } from "./columns";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const { loading, columns, dataList, pagination, Empty, onCurrentChange } =
useColumns();
</script>
<template>
<pure-table
row-key="id"
alignWhole="center"
showOverflowTooltip
:loading="loading"
:loading-config="{ background: 'transparent' }"
:data="
dataList.slice(
(pagination.currentPage - 1) * pagination.pageSize,
pagination.currentPage * pagination.pageSize
)
"
:columns="columns"
:pagination="pagination"
@page-current-change="onCurrentChange"
>
<template #empty>
<el-empty description="Không có dữ liệu" :image-size="60">
<template #image>
<Empty />
</template>
</el-empty>
</template>
<template #operation="{ row }">
<el-button
plain
circle
size="small"
:title="`Kiểm tra số sê-ri để${row.id}Chi tiết`"
:icon="useRenderIcon('ri:search-line')"
/>
</template>
</pure-table>
</template>
<style lang="scss">
.pure-table-filter {
.el-table-filter__list {
min-width: 80px;
padding: 0;
li {
line-height: 28px;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-table) {
--el-table-border: none;
--el-table-border-color: transparent;
.el-empty__description {
margin: 0;
}
.el-scrollbar__bar {
display: none;
}
}
</style>

142
src/views/welcome/data.ts Normal file
View File

@ -0,0 +1,142 @@
import { dayjs, cloneDeep, getRandomIntBetween } from "./utils";
import GroupLine from "@iconify-icons/ri/group-line";
import Question from "@iconify-icons/ri/question-answer-line";
import CheckLine from "@iconify-icons/ri/chat-check-line";
import Smile from "@iconify-icons/ri/star-smile-line";
const days = [
"Chủ Nhật",
"Thứ Hai",
"Thứ Ba",
"Thứ Tư",
"Thứ Năm",
"Thứ Sáu",
"Thứ Bảy"
];
/** Số lượng yêu cầu, số lượng câu hỏi, số lượng giải pháp, sự hài lòng của người dùng*/
const chartData = [
{
icon: GroupLine,
bgColor: "#effaff",
color: "#41b6ff",
duration: 2200,
name: "Số người cần thiết",
value: 36000,
percent: "+88%",
data: [2101, 5288, 4239, 4962, 6752, 5208, 7450] // Dữ liệu biểu đồ đường trơn
},
{
icon: Question,
bgColor: "#fff5f4",
color: "#e85f33",
duration: 1600,
name: "Số lượng các câu hỏi",
value: 16580,
percent: "+70%",
data: [2216, 1148, 1255, 788, 4821, 1973, 4379]
},
{
icon: CheckLine,
bgColor: "#eff8f4",
color: "#26ce83",
duration: 1500,
name: "số lượng giải ",
value: 16499,
percent: "+99%",
data: [861, 1002, 3195, 1715, 3666, 2415, 3645]
},
{
icon: Smile,
bgColor: "#f6f4fe",
color: "#7846e5",
duration: 100,
name: "sự hài lòng của khách hàng",
value: 100,
percent: "+100%",
data: [100]
}
];
/** Tổng quan về phân tích */
const barChartData = [
{
requireData: [2101, 5288, 4239, 4962, 6752, 5208, 7450],
questionData: [2216, 1148, 1255, 1788, 4821, 1973, 4379]
},
{
requireData: [2101, 3280, 4400, 4962, 5752, 6889, 7600],
questionData: [2116, 3148, 3255, 3788, 4821, 4970, 5390]
}
];
/** Xác xuất */
const progressData = [
{
week: "Thứ 2",
percentage: 85,
duration: 110,
color: "#41b6ff"
},
{
week: "Thứ 3",
percentage: 86,
duration: 105,
color: "#41b6ff"
},
{
week: "Thứ 4",
percentage: 88,
duration: 100,
color: "#41b6ff"
},
{
week: "Thứ 5",
percentage: 89,
duration: 95,
color: "#41b6ff"
},
{
week: "Thứ 6",
percentage: 94,
duration: 90,
color: "#26ce83"
},
{
week: "Thứ 7",
percentage: 96,
duration: 85,
color: "#26ce83"
},
{
week: "Chủ nhật",
percentage: 100,
duration: 80,
color: "#26ce83"
}
].reverse();
/** Số liệu thống kế */
const tableData = Array.from({ length: 30 }).map((_, index) => {
return {
id: index + 1,
requiredNumber: getRandomIntBetween(13500, 19999),
questionNumber: getRandomIntBetween(12600, 16999),
resolveNumber: getRandomIntBetween(13500, 17999),
satisfaction: getRandomIntBetween(95, 100),
date: dayjs().subtract(index, "day").format("YYYY-MM-DD")
};
});
/** Tin mới nhất */
const latestNewsData = cloneDeep(tableData)
.slice(0, 14)
.map((item, index) => {
return Object.assign(item, {
date: `${dayjs().subtract(index, "day").format("YYYY-MM-DD")} ${
days[dayjs().subtract(index, "day").day()]
}`
});
});
export { chartData, barChartData, progressData, tableData, latestNewsData };

View File

@ -1,9 +1,276 @@
<script setup lang="ts">
import { ref, markRaw } from "vue";
import ReCol from "@/components/ReCol";
import { useDark, randomGradient } from "./utils";
import WelcomeTable from "./components/table/index.vue";
import { ReNormalCountTo } from "@/components/ReCountTo";
import { useRenderFlicker } from "@/components/ReFlicker";
import { ChartBar, ChartLine, ChartRound } from "./components/charts";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { chartData, barChartData, progressData, latestNewsData } from "./data";
defineOptions({
name: "Welcome"
});
const { isDark } = useDark();
let curWeek = ref(1); // 0 tun trưc, 1 tun này
const optionsBasis: Array<OptionsType> = [
{
label: "Tuần trước"
},
{
label: "Tuần này"
}
];
</script>
<template>
<h1>Pure-Admin-Thin国际化版本</h1>
<div>
<el-row :gutter="24" justify="space-around">
<re-col
v-for="(item, index) in chartData"
:key="index"
v-motion
class="mb-[18px]"
:value="6"
:md="12"
:sm="12"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 80 * (index + 1)
}
}"
>
<el-card class="line-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">
{{ item.name }}
</span>
<div
class="w-8 h-8 flex justify-center items-center rounded-md"
:style="{
backgroundColor: isDark ? 'transparent' : item.bgColor
}"
>
<IconifyIconOffline
:icon="item.icon"
:color="item.color"
width="18"
/>
</div>
</div>
<div class="flex justify-between items-start mt-3">
<div class="w-1/2">
<ReNormalCountTo
:duration="item.duration"
:fontSize="'1.6em'"
:startVal="100"
:endVal="item.value"
/>
<p class="font-medium text-green-500">{{ item.percent }}</p>
</div>
<ChartLine
v-if="item.data.length > 1"
class="!w-1/2"
:color="item.color"
:data="item.data"
/>
<ChartRound v-else class="!w-1/2" />
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="18"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 400
}
}"
>
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">Tổng quan về phân tích</span>
<Segmented v-model="curWeek" :options="optionsBasis" />
</div>
<div class="flex justify-between items-start mt-3">
<ChartBar
:requireData="barChartData[curWeek].requireData"
:questionData="barChartData[curWeek].questionData"
/>
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="6"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 480
}
}"
>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">giải quyết xác suất</span>
</div>
<div
v-for="(item, index) in progressData"
:key="index"
:class="[
'flex',
'justify-between',
'items-start',
index === 0 ? 'mt-8' : 'mt-[2.15rem]'
]"
>
<el-progress
:text-inside="true"
:percentage="item.percentage"
:stroke-width="21"
:color="item.color"
striped
striped-flow
:duration="item.duration"
/>
<span class="text-nowrap ml-2 text-text_color_regular text-sm">
{{ item.week }}
</span>
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="18"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 560
}
}"
>
<el-card shadow="never" class="h-[580px]">
<div class="flex justify-between">
<span class="text-md font-medium">Số liệu thống </span>
</div>
<WelcomeTable class="mt-3" />
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="6"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 640
}
}"
>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">Tin mới nhất</span>
</div>
<el-scrollbar max-height="504" class="mt-3">
<el-timeline>
<el-timeline-item
v-for="(item, index) in latestNewsData"
:key="index"
center
placement="top"
:icon="
markRaw(
useRenderFlicker({
background: randomGradient({
randomizeHue: true
})
})
)
"
:timestamp="item.date"
>
<p class="text-text_color_regular text-sm">
{{
`新增 ${item.requiredNumber} 条问题,${item.resolveNumber} 条已解决`
}}
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
</re-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-card) {
--el-card-border-color: none;
/* 解决概率进度条宽度 */
.el-progress--line {
width: 85%;
}
/* 解决概率进度条字体大小 */
.el-progress-bar__innerText {
font-size: 15px;
}
/* 隐藏 el-scrollbar 滚动条 */
.el-scrollbar__bar {
display: none;
}
/* el-timeline 每一项上下、左右边距 */
.el-timeline-item {
margin: 0 6px;
}
}
.main-content {
margin: 20px 20px 0 !important;
}
</style>

View File

@ -0,0 +1,6 @@
export { default as dayjs } from "dayjs";
export { useDark, cloneDeep, randomGradient } from "@pureadmin/utils";
export function getRandomIntBetween(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

18
types/directives.d.ts vendored
View File

@ -3,21 +3,21 @@ import type { CopyEl, OptimizeOptions, RippleOptions } from "@/directives";
declare module "vue" {
export interface ComponentCustomProperties {
/** `Loading` 动画加载指令具体看https://element-plus.org/zh-CN/component/loading.html#%E6%8C%87%E4%BB%A4 */
/** Chỉ thị `Loading` cho hoạt ảnh tải, chi tiết xem tại: https://element-plus.org/en-US/component/loading.html
vLoading: Directive<Element, boolean>;
/** 按钮权限指令 */
/** Chỉ thị quyền nút */
vAuth: Directive<HTMLElement, string | Array<string>>;
/** 文本复制指令(默认双击复制) */
/** Chỉ thị sao chép văn bản (mặc định nhấp đúp để sao chép) */
vCopy: Directive<CopyEl, string>;
/** 长按指令 */
/** Chỉ thị nhấn giữ */
vLongpress: Directive<HTMLElement, Function>;
/** 防抖、节流指令 */
/** Chỉ thị chống rung, tiết lưu */
vOptimize: Directive<HTMLElement, OptimizeOptions>;
/**
* `v-ripple`
* 1. `v-ripple``ripple`
* 2. `v-ripple="{ class: 'text-red' }"``ripple``tailwindcss``color`
* 3. `v-ripple.center`
* Chỉ thị `v-ripple`, cách sử dụng như sau:
* 1. `v-ripple` đ kích hoạt tính năng `ripple` bản
* 2. `v-ripple="{ class: 'text-red' }"` đ tùy chỉnh màu `ripple`, hỗ trợ `tailwindcss`, màu hiệu lực `color`
* 3. `v-ripple.center` đ lan tỏa từ trung tâm
*/
vRipple: Directive<HTMLElement, RippleOptions>;
}

View File

@ -1,6 +1,6 @@
declare module "vue" {
/**
* Volar Volar
* Đăng các thành phần toàn cầu tùy chỉnh đ nhận đưc gợi ý từ Volar
*/
export interface GlobalComponents {
IconifyIconOffline: (typeof import("../src/components/ReIcon"))["IconifyIconOffline"];
@ -12,7 +12,7 @@ declare module "vue" {
/**
* TODO https://github.com/element-plus/element-plus/blob/dev/global.d.ts#L2
* No need to install @vue/runtime-core
* Không cần cài đt @vue/runtime-core
*/
declare module "vue" {
export interface GlobalComponents {

31
types/global.d.ts vendored
View File

@ -2,11 +2,12 @@ import type { ECharts } from "echarts";
import type { TableColumns } from "@pureadmin/table";
/**
* `.vue` `.ts` `.tsx` 使
* Khai báo toàn cầu, không cần nhập trực tiếp đ sử dụng gợi ý kiểu trong các tệp `.vue`, `.ts`, `.tsx`
*/
declare global {
/**
* `node``pnpm`
* Thông tin ng dụng như tên, phiên bản, yêu cầu phiên bản `node` `pnpm`, các phụ thuộc,
* các phụ thuộc phát triển thời gian xây dựng cuối cùng của .
*/
const __APP_INFO__: {
pkg: {
@ -23,10 +24,10 @@ declare global {
};
/**
* Window
* Kiểu gợi ý cho đi tượng Window
*/
interface Window {
// Global vue app instance
// Thể hiện ứng dụng Vue toàn cục
__APP__: App<Element>;
webkitCancelAnimationFrame: (handle: number) => void;
mozCancelAnimationFrame: (handle: number) => void;
@ -39,7 +40,7 @@ declare global {
}
/**
* Document
* Kiểu gợi ý cho đi tượng Document
*/
interface Document {
webkitFullscreenElement?: Element;
@ -48,7 +49,7 @@ declare global {
}
/**
*
* Kiểu gợi ý cho các lựa chọn nén đóng gói
*/
type ViteCompression =
| "none"
@ -60,8 +61,7 @@ declare global {
| "both-clear";
/**
*
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#%E5%85%B7%E4%BD%93%E9%85%8D%E7%BD%AE}
* Kiểu gợi ý cho các biến môi trường tùy chỉnh toàn cầu
*/
interface ViteEnv {
VITE_PORT: number;
@ -73,13 +73,12 @@ declare global {
}
/**
* `@pureadmin/table` `TableColumns` 便
* Mảng kiểu gợi ý cho các cột trong bảng, mở rộng từ `@pureadmin/table`
*/
interface TableColumnList extends Array<TableColumns> {}
/**
* `public/platform-config.json`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#platform-config-json}
* Kiểu dữ liệu tương ng với tệp `public/platform-config.json`
*/
interface PlatformConfigs {
Version?: string;
@ -111,8 +110,7 @@ declare global {
}
/**
* `PlatformConfigs`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#platform-config-json}
* Kiểu dữ liệu lưu trữ trong bộ nhớ cục bộ, khác biệt so với `PlatformConfigs`
*/
interface StorageConfigs {
version?: string;
@ -140,7 +138,7 @@ declare global {
}
/**
* `responsive-storage` `storage`
* Đi tượng lưu trữ phản ng cục bộ dựa trên `responsive-storage`
*/
interface ResponsiveStorage {
locale: {
@ -169,7 +167,7 @@ declare global {
}
/**
* 访
* Thuộc tính toàn cầu cho tất cả các thể hiện thành phần trong ng dụng
*/
interface GlobalPropertiesApi {
$echarts: ECharts;
@ -178,10 +176,9 @@ declare global {
}
/**
* `Element`
* Mở rộng đi tượng Element đ hỗ trợ v-ripple từ `src/directives/ripple/index.ts`
*/
interface Element {
// v-ripple 作用于 src/directives/ripple/index.ts 文件
_ripple?: {
enabled?: boolean;
centered?: boolean;

2
types/index.d.ts vendored
View File

@ -1,4 +1,4 @@
// 此文件跟同级目录的 global.d.ts 文件一样也是全局类型声明,只不过这里存放一些零散的全局类型,无需引入直接在 .vue 、.ts 、.tsx 文件使用即可获得类型提示
// Như tệp global.d.ts cùng cấp với nó, tệp này cũng là khai báo toàn cầu, nhưng lưu trữ một số loại toàn cầu phân tán. Không cần nhập, bạn có thể sử dụng trực tiếp trong các tệp .vue, .ts, .tsx để nhận được gợi ý kiểu
type RefType<T> = T | null;

76
types/router.d.ts vendored
View File

@ -1,4 +1,4 @@
// 全局路由类型声明
// Khai báo toàn cầu cho loại định tuyến
import type { RouteComponent, RouteLocationNormalized } from "vue-router";
import type { FunctionalComponent } from "vue";
@ -9,95 +9,95 @@ declare global {
}
/**
* @description `meta`
* @description Bảng cấu hình `meta` cho đnh tuyến con đy đ
*/
interface CustomizeRouteMeta {
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
/** Tên của menu (hỗ trợ cả quốc tế hóa và không quốc tế hóa, nếu sử dụng cách viết quốc tế hóa, cần thêm vào thư mục `locales` ở thư mục gốc) `Bắt buộc` */
title: string;
/** 菜单图标 `可选` */
/** Biểu tượng của menu `Tùy chọn` */
icon?: string | FunctionalComponent | IconifyIcon;
/** 菜单名称右侧的额外图标 */
/** Biểu tượng bổ sung bên phải của tên menu */
extraIcon?: string | FunctionalComponent | IconifyIcon;
/** 是否在菜单中显示(默认`true``可选` */
/** Hiển thị trong menu hay không (mặc định là `true`) `Tùy chọn` */
showLink?: boolean;
/** 是否显示父级菜单 `可选` */
/** Hiển thị menu cha hay không `Tùy chọn` */
showParent?: boolean;
/** 页面级别权限设置 `可选` */
/** Cài đặt quyền trang `Tùy chọn` */
roles?: Array<string>;
/** 按钮级别权限设置 `可选` */
/** Cài đặt quyền nút `Tùy chọn` */
auths?: Array<string>;
/** 路由组件缓存(开启 `true`、关闭 `false``可选` */
/** Bật/tắt bộ nhớ đệm cho thành phần định tuyến (bật `true`, tắt `false`) `Tùy chọn` */
keepAlive?: boolean;
/** 内嵌的`iframe`链接 `可选` */
/** Liên kết `iframe` nhúng `Tùy chọn` */
frameSrc?: string;
/** `iframe`页是否开启首次加载动画(默认`true``可选` */
/** Bật/tắt hoạt hình tải lần đầu cho `iframe` (mặc định là `true`) `Tùy chọn` */
frameLoading?: boolean;
/** 页面加载动画(两种模式,第二种权重更高,第一种直接采用`vue`内置的`transitions`动画,第二种是使用`animate.css`编写进、离场动画,平台更推荐使用第二种模式,已经内置了`animate.css`,直接写对应的动画名即可)`可选` */
/** Hoạt hình tải trang (hai chế độ, chế độ thứ hai có ưu tiên hơn, chế độ thứ nhất sử dụng hoạt hình `transitions` có sẵn trong Vue, chế độ thứ hai sử dụng hoạt hình `animate.css`, nền tảng khuyến khích sử dụng chế độ thứ hai với `animate.css` đã được tích hợp sẵn, chỉ cần viết tên hoạt hình tương ứng) `Tùy chọn` */
transition?: {
/**
* @description
* @description Hiệu ng hoạt hình cho đnh tuyến hiện tại
* @see {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
* @see animate.css {@link https://animate.style}
*/
name?: string;
/** 进场动画 */
/** Hiệu ứng khi vào */
enterTransition?: string;
/** 离场动画 */
/** Hiệu ứng khi ra đi */
leaveTransition?: string;
};
/** 当前菜单名称或自定义信息禁止添加到标签页(默认`false` */
/** Không cho phép thêm tên menu hiện tại hoặc thông tin tùy chỉnh vào tab (mặc định là `false`) */
hiddenTag?: boolean;
/** 当前菜单名称是否固定显示在标签页且不可关闭(默认`false` */
/** Cố định hiển thị tên menu hiện tại trên tab và không thể đóng (mặc định là `false`) */
fixedTag?: boolean;
/** 动态路由可打开的最大数量 `可选` */
/** Số lượng tối đa của độ sâu định tuyến có thể mở `Tùy chọn` */
dynamicLevel?: number;
/**
* `query``params``showLink: false`
* `activePath``activePath``path`
/** Kích hot mt menu c th
* (chủ yếu dùng cho đnh tuyến truyền tham số qua `query` hoặc `params`, khi chúng không hiển thị trong menu với `showLink: false`, không bất kỳ menu nào đưc làm nổi bật,
* thay vào đó chỉ cần đt `activePath` đ chỉ đnh menu cần kích hoạt, `activePath` `path` của menu cần kích hoạt)
*/
activePath?: string;
}
/**
* @description
* @description Bảng cấu hình đnh tuyến con đy đ
*/
interface RouteChildrenConfigsTable {
/** 子路由地址 `必填` */
/** Địa chỉ định tuyến con `Bắt buộc` */
path: string;
/** 路由名字(对应不要重复,和当前组件的`name`保持一致)`必填` */
/** Tên định tuyến (đảm bảo duy nhất, tên phải trùng với `name` của thành phần hiện tại) `Bắt buộc` */
name?: string;
/** 路由重定向 `可选` */
/** Điều hướng lại định tuyến `Tùy chọn` */
redirect?: string;
/** 按需加载组件 `可选` */
/** Thành phần tải theo yêu cầu `Tùy chọn` */
component?: RouteComponent;
meta?: CustomizeRouteMeta;
/** 子路由配置项 */
/** Các mục cấu hình định tuyến con */
children?: Array<RouteChildrenConfigsTable>;
}
/**
* @description
* @description Bảng cấu hình toàn bộ đnh tuyến (bao gồm cả đnh tuyến con đy đ)
*/
interface RouteConfigsTable {
/** 路由地址 `必填` */
/** Địa chỉ định tuyến `Bắt buộc` */
path: string;
/** 路由名字(保持唯一)`可选` */
/** Tên định tuyến (duy trì duy nhất) `Tùy chọn` */
name?: string;
/** `Layout`组件 `可选` */
/** Thành phần `Layout` `Tùy chọn` */
component?: RouteComponent;
/** 路由重定向 `可选` */
/** Điều hướng lại định tuyến `Tùy chọn` */
redirect?: string;
meta?: {
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
/** Tên menu (hỗ trợ cả quốc tế hóa và không quốc tế hóa, nếu sử dụng cách viết quốc tế hóa, cần thêm vào thư mục `locales` ở thư mục gốc) `Bắt buộc` */
title: string;
/** 菜单图标 `可选` */
/** Biểu tượng của menu `Tùy chọn` */
icon?: string | FunctionalComponent | IconifyIcon;
/** 是否在菜单中显示(默认`true``可选` */
/** Hiển thị trong menu hay không (mặc định là `true`) `Tùy chọn` */
showLink?: boolean;
/** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
/** Sắp xếp thứ tự menu, giá trị càng cao sắp xếp càng sau (chỉ áp dụng cho định tuyến cấp đầu) `Tùy chọn` */
rank?: number;
};
/** 子路由配置项 */
/** Các mục cấu hình định tuyến con */
children?: Array<RouteChildrenConfigsTable>;
}
}

View File

@ -18,14 +18,14 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
resolve: {
alias
},
// 服务端渲染
// Server-side rendering
server: {
// 端口号
// Port number
port: VITE_PORT,
host: "0.0.0.0",
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
// Local cross-origin proxy https://cn.vitejs.dev/config/server-options.html#server-proxy
proxy: {},
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
// Preheat files to pre-transform and cache results, reducing initial page load time during startup and preventing transformation waterfall
warmup: {
clientFiles: ["./index.html", "./src/{views,components}/*"]
}
@ -40,13 +40,13 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
// https://cn.vitejs.dev/guide/build.html#browser-compatibility
target: "es2015",
sourcemap: false,
// 消除打包大小超过500kb警告
// Eliminate package size warning over 500kb
chunkSizeWarningLimit: 4000,
rollupOptions: {
input: {
index: pathResolve("./index.html", import.meta.url)
},
// 静态资源分类打包
// Static resource classification packaging
output: {
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",